diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp
index 70eece9af10d3aa218a0af3db961f4305f0507d9..4e266cdad252a1872472354d72843ec7a21479cb 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp
@@ -1098,6 +1098,9 @@ void RasterizerOpenGL::SetupImage(u32 binding, const Tegra::Texture::TICEntry& t
     if (!tic.IsBuffer()) {
         view->ApplySwizzle(tic.x_source, tic.y_source, tic.z_source, tic.w_source);
     }
+    if (entry.IsWritten()) {
+        view->MarkAsModified(texture_cache.Tick());
+    }
     state.images[binding] = view->GetTexture();
 }
 
diff --git a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp
index a5cc1a86f2fcd6776d57836bb745f2a0a3d5dfc1..6edb2ca38314fd60d6675565d902dc38fc9b6752 100644
--- a/src/video_core/renderer_opengl/gl_shader_decompiler.cpp
+++ b/src/video_core/renderer_opengl/gl_shader_decompiler.cpp
@@ -389,11 +389,10 @@ public:
         for (const auto& sampler : ir.GetSamplers()) {
             entries.samplers.emplace_back(sampler);
         }
-        for (const auto& image : ir.GetImages()) {
+        for (const auto& [offset, image] : ir.GetImages()) {
             entries.images.emplace_back(image);
         }
-        for (const auto& gmem_pair : ir.GetGlobalMemory()) {
-            const auto& [base, usage] = gmem_pair;
+        for (const auto& [base, usage] : ir.GetGlobalMemory()) {
             entries.global_memory_entries.emplace_back(base.cbuf_index, base.cbuf_offset,
                                                        usage.is_read, usage.is_written);
         }
@@ -706,7 +705,7 @@ private:
 
     void DeclareImages() {
         const auto& images{ir.GetImages()};
-        for (const auto& image : images) {
+        for (const auto& [offset, image] : images) {
             const std::string image_type = [&]() {
                 switch (image.GetType()) {
                 case Tegra::Shader::ImageType::Texture1D:
@@ -726,9 +725,16 @@ private:
                     return "image1D";
                 }
             }();
-            code.AddLine("layout (binding = IMAGE_BINDING_{}) coherent volatile writeonly uniform "
+            std::string qualifier = "coherent volatile";
+            if (image.IsRead() && !image.IsWritten()) {
+                qualifier += " readonly";
+            } else if (image.IsWritten() && !image.IsRead()) {
+                qualifier += " writeonly";
+            }
+
+            code.AddLine("layout (binding = IMAGE_BINDING_{}) {} uniform "
                          "{} {};",
-                         image.GetIndex(), image_type, GetImage(image));
+                         image.GetIndex(), qualifier, image_type, GetImage(image));
         }
         if (!images.empty()) {
             code.AddNewLine();
diff --git a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp
index 969fe9ced26837c2be68b3205f057ddfec93c5e3..5450feedf48410f6957d9a6ca60f6fb198832006 100644
--- a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp
@@ -341,13 +341,16 @@ std::optional<ShaderDiskCacheDecompiled> ShaderDiskCacheOpenGL::LoadDecompiledEn
         u64 index{};
         u32 type{};
         u8 is_bindless{};
+        u8 is_read{};
+        u8 is_written{};
         if (!LoadObjectFromPrecompiled(offset) || !LoadObjectFromPrecompiled(index) ||
-            !LoadObjectFromPrecompiled(type) || !LoadObjectFromPrecompiled(is_bindless)) {
+            !LoadObjectFromPrecompiled(type) || !LoadObjectFromPrecompiled(is_bindless) ||
+            !LoadObjectFromPrecompiled(is_read) || !LoadObjectFromPrecompiled(is_written)) {
             return {};
         }
-        entry.entries.images.emplace_back(
-            static_cast<std::size_t>(offset), static_cast<std::size_t>(index),
-            static_cast<Tegra::Shader::ImageType>(type), is_bindless != 0);
+        entry.entries.images.emplace_back(static_cast<u64>(offset), static_cast<std::size_t>(index),
+                                          static_cast<Tegra::Shader::ImageType>(type),
+                                          is_bindless != 0, is_written != 0, is_read != 0);
     }
 
     u32 global_memory_count{};
@@ -429,7 +432,9 @@ bool ShaderDiskCacheOpenGL::SaveDecompiledFile(u64 unique_identifier, const std:
         if (!SaveObjectToPrecompiled(static_cast<u64>(image.GetOffset())) ||
             !SaveObjectToPrecompiled(static_cast<u64>(image.GetIndex())) ||
             !SaveObjectToPrecompiled(static_cast<u32>(image.GetType())) ||
-            !SaveObjectToPrecompiled(static_cast<u8>(image.IsBindless() ? 1 : 0))) {
+            !SaveObjectToPrecompiled(static_cast<u8>(image.IsBindless() ? 1 : 0)) ||
+            !SaveObjectToPrecompiled(static_cast<u8>(image.IsRead() ? 1 : 0)) ||
+            !SaveObjectToPrecompiled(static_cast<u8>(image.IsWritten() ? 1 : 0))) {
             return false;
         }
     }
diff --git a/src/video_core/renderer_opengl/gl_texture_cache.h b/src/video_core/renderer_opengl/gl_texture_cache.h
index 21324488aac17d04842161dbc4cf08f3f016f1db..8e13ab38bb56684c82b186da2b8557db7f3fa8e3 100644
--- a/src/video_core/renderer_opengl/gl_texture_cache.h
+++ b/src/video_core/renderer_opengl/gl_texture_cache.h
@@ -78,6 +78,17 @@ public:
     /// Attaches this texture view to the current bound GL_DRAW_FRAMEBUFFER
     void Attach(GLenum attachment, GLenum target) const;
 
+    void ApplySwizzle(Tegra::Texture::SwizzleSource x_source,
+                      Tegra::Texture::SwizzleSource y_source,
+                      Tegra::Texture::SwizzleSource z_source,
+                      Tegra::Texture::SwizzleSource w_source);
+
+    void DecorateViewName(GPUVAddr gpu_addr, std::string prefix);
+
+    void MarkAsModified(u64 tick) {
+        surface.MarkAsModified(true, tick);
+    }
+
     GLuint GetTexture() const {
         if (is_proxy) {
             return surface.GetTexture();
@@ -89,13 +100,6 @@ public:
         return surface.GetSurfaceParams();
     }
 
-    void ApplySwizzle(Tegra::Texture::SwizzleSource x_source,
-                      Tegra::Texture::SwizzleSource y_source,
-                      Tegra::Texture::SwizzleSource z_source,
-                      Tegra::Texture::SwizzleSource w_source);
-
-    void DecorateViewName(GPUVAddr gpu_addr, std::string prefix);
-
 private:
     u32 EncodeSwizzle(Tegra::Texture::SwizzleSource x_source,
                       Tegra::Texture::SwizzleSource y_source,
@@ -111,8 +115,8 @@ private:
     GLenum target{};
 
     OGLTextureView texture_view;
-    u32 swizzle;
-    bool is_proxy;
+    u32 swizzle{};
+    bool is_proxy{};
 };
 
 class TextureCacheOpenGL final : public TextureCacheBase {
diff --git a/src/video_core/shader/decode/image.cpp b/src/video_core/shader/decode/image.cpp
index 77151a24beaabc55d074fb11aafd8f6cf0ad029d..008109a9902462dc4459e4d82dd56755c7749122 100644
--- a/src/video_core/shader/decode/image.cpp
+++ b/src/video_core/shader/decode/image.cpp
@@ -61,56 +61,54 @@ u32 ShaderIR::DecodeImage(NodeBlock& bb, u32 pc) {
         }
 
         const auto type{instr.sust.image_type};
-        const auto& image{instr.sust.is_immediate ? GetImage(instr.image, type)
-                                                  : GetBindlessImage(instr.gpr39, type)};
+        auto& image{instr.sust.is_immediate ? GetImage(instr.image, type)
+                                            : GetBindlessImage(instr.gpr39, type)};
+        image.MarkWrite();
+
         MetaImage meta{image, values};
         const Node store{Operation(OperationCode::ImageStore, meta, std::move(coords))};
         bb.push_back(store);
         break;
     }
     default:
-        UNIMPLEMENTED_MSG("Unhandled conversion instruction: {}", opcode->get().GetName());
+        UNIMPLEMENTED_MSG("Unhandled image instruction: {}", opcode->get().GetName());
     }
 
     return pc;
 }
 
-const Image& ShaderIR::GetImage(Tegra::Shader::Image image, Tegra::Shader::ImageType type) {
-    const auto offset{static_cast<std::size_t>(image.index.Value())};
+Image& ShaderIR::GetImage(Tegra::Shader::Image image, Tegra::Shader::ImageType type) {
+    const auto offset{static_cast<u64>(image.index.Value())};
 
     // If this image has already been used, return the existing mapping.
-    const auto itr{std::find_if(used_images.begin(), used_images.end(),
-                                [=](const Image& entry) { return entry.GetOffset() == offset; })};
-    if (itr != used_images.end()) {
-        ASSERT(itr->GetType() == type);
-        return *itr;
+    const auto it = used_images.find(offset);
+    if (it != used_images.end()) {
+        ASSERT(it->second.GetType() == type);
+        return it->second;
     }
 
     // Otherwise create a new mapping for this image.
     const std::size_t next_index{used_images.size()};
-    const Image entry{offset, next_index, type};
-    return *used_images.emplace(entry).first;
+    return used_images.emplace(offset, Image{offset, next_index, type}).first->second;
 }
 
-const Image& ShaderIR::GetBindlessImage(Tegra::Shader::Register reg,
-                                        Tegra::Shader::ImageType type) {
+Image& ShaderIR::GetBindlessImage(Tegra::Shader::Register reg, Tegra::Shader::ImageType type) {
     const Node image_register{GetRegister(reg)};
     const auto [base_image, cbuf_index, cbuf_offset]{
         TrackCbuf(image_register, global_code, static_cast<s64>(global_code.size()))};
     const auto cbuf_key{(static_cast<u64>(cbuf_index) << 32) | static_cast<u64>(cbuf_offset)};
 
     // If this image has already been used, return the existing mapping.
-    const auto itr{std::find_if(used_images.begin(), used_images.end(),
-                                [=](const Image& entry) { return entry.GetOffset() == cbuf_key; })};
-    if (itr != used_images.end()) {
-        ASSERT(itr->GetType() == type);
-        return *itr;
+    const auto it = used_images.find(cbuf_key);
+    if (it != used_images.end()) {
+        ASSERT(it->second.GetType() == type);
+        return it->second;
     }
 
     // Otherwise create a new mapping for this image.
     const std::size_t next_index{used_images.size()};
-    const Image entry{cbuf_index, cbuf_offset, next_index, type};
-    return *used_images.emplace(entry).first;
+    return used_images.emplace(cbuf_key, Image{cbuf_index, cbuf_offset, next_index, type})
+        .first->second;
 }
 
 } // namespace VideoCommon::Shader
diff --git a/src/video_core/shader/node.h b/src/video_core/shader/node.h
index 0397f4c6e045f8af37827d7a8b84f5953c313a85..b29aedce8d835342d361330b9763c08e5ec3c5ce 100644
--- a/src/video_core/shader/node.h
+++ b/src/video_core/shader/node.h
@@ -273,50 +273,64 @@ private:
     bool is_bindless{}; ///< Whether this sampler belongs to a bindless texture or not.
 };
 
-class Image {
+class Image final {
 public:
-    explicit Image(std::size_t offset, std::size_t index, Tegra::Shader::ImageType type)
+    constexpr explicit Image(u64 offset, std::size_t index, Tegra::Shader::ImageType type)
         : offset{offset}, index{index}, type{type}, is_bindless{false} {}
 
-    explicit Image(u32 cbuf_index, u32 cbuf_offset, std::size_t index,
-                   Tegra::Shader::ImageType type)
+    constexpr explicit Image(u32 cbuf_index, u32 cbuf_offset, std::size_t index,
+                             Tegra::Shader::ImageType type)
         : offset{(static_cast<u64>(cbuf_index) << 32) | cbuf_offset}, index{index}, type{type},
           is_bindless{true} {}
 
-    explicit Image(std::size_t offset, std::size_t index, Tegra::Shader::ImageType type,
-                   bool is_bindless)
-        : offset{offset}, index{index}, type{type}, is_bindless{is_bindless} {}
+    constexpr explicit Image(std::size_t offset, std::size_t index, Tegra::Shader::ImageType type,
+                             bool is_bindless, bool is_written, bool is_read)
+        : offset{offset}, index{index}, type{type}, is_bindless{is_bindless},
+          is_written{is_written}, is_read{is_read} {}
 
-    std::size_t GetOffset() const {
+    void MarkRead() {
+        is_read = true;
+    }
+
+    void MarkWrite() {
+        is_written = true;
+    }
+
+    constexpr std::size_t GetOffset() const {
         return offset;
     }
 
-    std::size_t GetIndex() const {
+    constexpr std::size_t GetIndex() const {
         return index;
     }
 
-    Tegra::Shader::ImageType GetType() const {
+    constexpr Tegra::Shader::ImageType GetType() const {
         return type;
     }
 
-    bool IsBindless() const {
+    constexpr bool IsBindless() const {
         return is_bindless;
     }
 
-    std::pair<u32, u32> GetBindlessCBuf() const {
-        return {static_cast<u32>(offset >> 32), static_cast<u32>(offset)};
+    constexpr bool IsRead() const {
+        return is_read;
+    }
+
+    constexpr bool IsWritten() const {
+        return is_written;
     }
 
-    bool operator<(const Image& rhs) const {
-        return std::tie(offset, index, type, is_bindless) <
-               std::tie(rhs.offset, rhs.index, rhs.type, rhs.is_bindless);
+    constexpr std::pair<u32, u32> GetBindlessCBuf() const {
+        return {static_cast<u32>(offset >> 32), static_cast<u32>(offset)};
     }
 
 private:
-    std::size_t offset{};
+    u64 offset{};
     std::size_t index{};
     Tegra::Shader::ImageType type{};
     bool is_bindless{};
+    bool is_read{};
+    bool is_written{};
 };
 
 struct GlobalMemoryBase {
diff --git a/src/video_core/shader/shader_ir.h b/src/video_core/shader/shader_ir.h
index bcc9b79b67b53c0926c4f568ae0d79dae144de39..0f891eace6680636b6627e9d0caec2d473b8ca38 100644
--- a/src/video_core/shader/shader_ir.h
+++ b/src/video_core/shader/shader_ir.h
@@ -95,7 +95,7 @@ public:
         return used_samplers;
     }
 
-    const std::set<Image>& GetImages() const {
+    const std::map<u64, Image>& GetImages() const {
         return used_images;
     }
 
@@ -272,10 +272,10 @@ private:
                                       bool is_shadow);
 
     /// Accesses an image.
-    const Image& GetImage(Tegra::Shader::Image image, Tegra::Shader::ImageType type);
+    Image& GetImage(Tegra::Shader::Image image, Tegra::Shader::ImageType type);
 
     /// Access a bindless image sampler.
-    const Image& GetBindlessImage(Tegra::Shader::Register reg, Tegra::Shader::ImageType type);
+    Image& GetBindlessImage(Tegra::Shader::Register reg, Tegra::Shader::ImageType type);
 
     /// Extracts a sequence of bits from a node
     Node BitfieldExtract(Node value, u32 offset, u32 bits);
@@ -356,7 +356,7 @@ private:
     std::set<Tegra::Shader::Attribute::Index> used_output_attributes;
     std::map<u32, ConstBuffer> used_cbufs;
     std::set<Sampler> used_samplers;
-    std::set<Image> used_images;
+    std::map<u64, Image> used_images;
     std::array<bool, Tegra::Engines::Maxwell3D::Regs::NumClipDistances> used_clip_distances{};
     std::map<GlobalMemoryBase, GlobalMemoryUsage> used_global_memory;
     bool uses_layer{};