diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp
index 2fb8ec33b..a1675355e 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp
@@ -99,11 +99,12 @@ void oglEnablei(GLenum cap, bool state, GLuint index) {
 } // Anonymous namespace
 
 RasterizerOpenGL::RasterizerOpenGL(Core::System& system, Core::Frontend::EmuWindow& emu_window,
-                                   ScreenInfo& info, GLShader::ProgramManager& program_manager)
-    : RasterizerAccelerated{system.Memory()}, texture_cache{system, *this, device},
+                                   ScreenInfo& info, GLShader::ProgramManager& program_manager,
+                                   StateTracker& state_tracker)
+    : RasterizerAccelerated{system.Memory()}, texture_cache{system, *this, device, state_tracker},
       shader_cache{*this, system, emu_window, device}, query_cache{system, *this}, system{system},
-      screen_info{info}, program_manager{program_manager}, buffer_cache{*this, system, device,
-                                                                        STREAM_BUFFER_SIZE} {
+      screen_info{info}, program_manager{program_manager}, state_tracker{state_tracker},
+      buffer_cache{*this, system, device, STREAM_BUFFER_SIZE} {
     CheckExtensions();
 }
 
@@ -320,9 +321,17 @@ void RasterizerOpenGL::LoadDiskResources(const std::atomic_bool& stop_loading,
     shader_cache.LoadDiskCache(stop_loading, callback);
 }
 
+void RasterizerOpenGL::SetupDirtyFlags() {
+    state_tracker.Initialize();
+}
+
 void RasterizerOpenGL::ConfigureFramebuffers() {
     MICROPROFILE_SCOPE(OpenGL_Framebuffer);
     auto& gpu = system.GPU().Maxwell3D();
+    if (!gpu.dirty.flags[VideoCommon::Dirty::RenderTargets]) {
+        return;
+    }
+    gpu.dirty.flags[VideoCommon::Dirty::RenderTargets] = false;
 
     texture_cache.GuardRenderTargets(true);
 
@@ -361,8 +370,6 @@ void RasterizerOpenGL::ConfigureFramebuffers() {
 
 void RasterizerOpenGL::ConfigureClearFramebuffer(bool using_color_fb, bool using_depth_fb,
                                                  bool using_stencil_fb) {
-    using VideoCore::Surface::SurfaceType;
-
     auto& gpu = system.GPU().Maxwell3D();
     const auto& regs = gpu.regs;
 
@@ -381,6 +388,7 @@ void RasterizerOpenGL::ConfigureClearFramebuffer(bool using_color_fb, bool using
     key.colors[0] = color_surface;
     key.zeta = depth_surface;
 
+    state_tracker.NotifyFramebuffer();
     glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer_cache.GetFramebuffer(key));
 }
 
diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h
index 48443bdff..22a3a3352 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer.h
+++ b/src/video_core/renderer_opengl/gl_rasterizer.h
@@ -30,6 +30,7 @@
 #include "video_core/renderer_opengl/gl_shader_cache.h"
 #include "video_core/renderer_opengl/gl_shader_decompiler.h"
 #include "video_core/renderer_opengl/gl_shader_manager.h"
+#include "video_core/renderer_opengl/gl_state_tracker.h"
 #include "video_core/renderer_opengl/gl_texture_cache.h"
 #include "video_core/renderer_opengl/utils.h"
 #include "video_core/textures/texture.h"
@@ -54,7 +55,8 @@ struct DrawParameters;
 class RasterizerOpenGL : public VideoCore::RasterizerAccelerated {
 public:
     explicit RasterizerOpenGL(Core::System& system, Core::Frontend::EmuWindow& emu_window,
-                              ScreenInfo& info, GLShader::ProgramManager& program_manager);
+                              ScreenInfo& info, GLShader::ProgramManager& program_manager,
+                              StateTracker& state_tracker);
     ~RasterizerOpenGL() override;
 
     void Draw(bool is_indexed, bool is_instanced) override;
@@ -75,6 +77,7 @@ public:
                            u32 pixel_stride) override;
     void LoadDiskResources(const std::atomic_bool& stop_loading,
                            const VideoCore::DiskResourceLoadCallback& callback) override;
+    void SetupDirtyFlags() override;
 
     /// Returns true when there are commands queued to the OpenGL server.
     bool AnyCommandQueued() const {
@@ -216,6 +219,7 @@ private:
     Core::System& system;
     ScreenInfo& screen_info;
     GLShader::ProgramManager& program_manager;
+    StateTracker& state_tracker;
 
     static constexpr std::size_t STREAM_BUFFER_SIZE = 128 * 1024 * 1024;
     OGLBufferCache buffer_cache;
diff --git a/src/video_core/renderer_opengl/gl_state_tracker.cpp b/src/video_core/renderer_opengl/gl_state_tracker.cpp
index e69de29bb..268b9351e 100644
--- a/src/video_core/renderer_opengl/gl_state_tracker.cpp
+++ b/src/video_core/renderer_opengl/gl_state_tracker.cpp
@@ -0,0 +1,85 @@
+// Copyright 2019 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <type_traits>
+
+#include "common/common_types.h"
+#include "core/core.h"
+#include "video_core/engines/maxwell_3d.h"
+#include "video_core/gpu.h"
+#include "video_core/renderer_opengl/gl_state_tracker.h"
+
+#define OFF(field_name) MAXWELL3D_REG_INDEX(field_name)
+#define NUM(field_name) (sizeof(Maxwell3D::Regs::field_name) / sizeof(u32))
+
+namespace OpenGL {
+
+namespace {
+
+using namespace Dirty;
+using namespace VideoCommon::Dirty;
+using Tegra::Engines::Maxwell3D;
+using Regs = Maxwell3D::Regs;
+using Dirty = std::remove_reference_t<decltype(Maxwell3D::dirty)>;
+using Tables = std::remove_reference_t<decltype(Maxwell3D::dirty.tables)>;
+using Table = std::remove_reference_t<decltype(Maxwell3D::dirty.tables[0])>;
+
+template <typename Integer>
+void FillBlock(Table& table, std::size_t begin, std::size_t num, Integer dirty_index) {
+    const auto it = std::begin(table) + begin;
+    std::fill(it, it + num, static_cast<u8>(dirty_index));
+}
+
+template <typename Integer1, typename Integer2>
+void FillBlock(Tables& tables, std::size_t begin, std::size_t num, Integer1 index_a,
+               Integer2 index_b) {
+    FillBlock(tables[0], begin, num, index_a);
+    FillBlock(tables[1], begin, num, index_b);
+}
+
+void SetupDirtyRenderTargets(Tables& tables) {
+    static constexpr std::size_t num_per_rt = NUM(rt[0]);
+    static constexpr std::size_t begin = OFF(rt);
+    static constexpr std::size_t num = num_per_rt * Regs::NumRenderTargets;
+    for (std::size_t rt = 0; rt < Regs::NumRenderTargets; ++rt) {
+        FillBlock(tables[0], begin + rt * num_per_rt, num_per_rt, ColorBuffer0 + rt);
+    }
+    FillBlock(tables[1], begin, num, RenderTargets);
+
+    static constexpr std::array zeta_flags{ZetaBuffer, RenderTargets};
+    for (std::size_t i = 0; i < std::size(zeta_flags); ++i) {
+        const u8 flag = zeta_flags[i];
+        auto& table = tables[i];
+        table[OFF(zeta_enable)] = flag;
+        table[OFF(zeta_width)] = flag;
+        table[OFF(zeta_height)] = flag;
+        FillBlock(table, OFF(zeta), NUM(zeta), flag);
+    }
+}
+
+} // Anonymous namespace
+
+StateTracker::StateTracker(Core::System& system) : system{system} {}
+
+void StateTracker::Initialize() {
+    auto& dirty = system.GPU().Maxwell3D().dirty;
+    std::size_t entry_index = 0;
+    const auto AddEntry = [&dirty, &entry_index](std::size_t dirty_register) {
+        dirty.on_write_stores[entry_index++] = static_cast<u8>(dirty_register);
+    };
+
+    AddEntry(RenderTargets);
+    for (std::size_t i = 0; i < Regs::NumRenderTargets; ++i) {
+        AddEntry(ColorBuffer0 + i);
+    }
+    AddEntry(ZetaBuffer);
+
+    auto& tables = dirty.tables;
+    SetupDirtyRenderTargets(tables);
+}
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/gl_state_tracker.h b/src/video_core/renderer_opengl/gl_state_tracker.h
index e69de29bb..91d4bb8d3 100644
--- a/src/video_core/renderer_opengl/gl_state_tracker.h
+++ b/src/video_core/renderer_opengl/gl_state_tracker.h
@@ -0,0 +1,56 @@
+// Copyright 2019 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "common/common_types.h"
+#include "video_core/dirty_flags.h"
+#include "video_core/engines/maxwell_3d.h"
+
+namespace Core {
+class System;
+}
+
+namespace OpenGL {
+
+namespace Dirty {
+enum : u8 {
+    First = VideoCommon::Dirty::LastCommonEntry,
+
+    VertexFormats,
+    VertexBuffers,
+    VertexInstances,
+    Shaders,
+    Viewports,
+    CullTestEnable,
+    FrontFace,
+    CullFace,
+    PrimitiveRestart,
+    DepthTest,
+    StencilTest,
+    ColorMask,
+    BlendState,
+    PolygonOffset,
+
+    VertexBuffer0 = PolygonOffset + 8,
+    VertexInstance0 = VertexBuffer0 + 32,
+};
+}
+
+class StateTracker {
+public:
+    explicit StateTracker(Core::System& system);
+
+    void Initialize();
+
+    void NotifyFramebuffer() {
+        auto& flags = system.GPU().Maxwell3D().dirty.flags;
+        flags[VideoCommon::Dirty::RenderTargets] = true;
+    }
+
+private:
+    Core::System& system;
+};
+
+} // namespace OpenGL
diff --git a/src/video_core/renderer_opengl/gl_texture_cache.cpp b/src/video_core/renderer_opengl/gl_texture_cache.cpp
index e2a58f0ad..1cadcf287 100644
--- a/src/video_core/renderer_opengl/gl_texture_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_texture_cache.cpp
@@ -10,6 +10,7 @@
 #include "core/core.h"
 #include "video_core/morton.h"
 #include "video_core/renderer_opengl/gl_resource_manager.h"
+#include "video_core/renderer_opengl/gl_state_tracker.h"
 #include "video_core/renderer_opengl/gl_texture_cache.h"
 #include "video_core/renderer_opengl/utils.h"
 #include "video_core/texture_cache/surface_base.h"
@@ -479,8 +480,8 @@ OGLTextureView CachedSurfaceView::CreateTextureView() const {
 
 TextureCacheOpenGL::TextureCacheOpenGL(Core::System& system,
                                        VideoCore::RasterizerInterface& rasterizer,
-                                       const Device& device)
-    : TextureCacheBase{system, rasterizer} {
+                                       const Device& device, StateTracker& state_tracker)
+    : TextureCacheBase{system, rasterizer}, state_tracker{state_tracker} {
     src_framebuffer.Create();
     dst_framebuffer.Create();
 }
@@ -518,6 +519,8 @@ void TextureCacheOpenGL::ImageBlit(View& src_view, View& dst_view,
     UNIMPLEMENTED_IF(dst_params.target == SurfaceTarget::Texture3D);
 
     // TODO: Signal state tracker about these changes
+    state_tracker.NotifyFramebuffer();
+
     if (dst_params.srgb_conversion) {
         glEnable(GL_FRAMEBUFFER_SRGB);
     } else {
diff --git a/src/video_core/renderer_opengl/gl_texture_cache.h b/src/video_core/renderer_opengl/gl_texture_cache.h
index 303534ca6..6658c6ffd 100644
--- a/src/video_core/renderer_opengl/gl_texture_cache.h
+++ b/src/video_core/renderer_opengl/gl_texture_cache.h
@@ -27,6 +27,7 @@ using VideoCommon::ViewParams;
 class CachedSurfaceView;
 class CachedSurface;
 class TextureCacheOpenGL;
+class StateTracker;
 
 using Surface = std::shared_ptr<CachedSurface>;
 using View = std::shared_ptr<CachedSurfaceView>;
@@ -127,7 +128,7 @@ private:
 class TextureCacheOpenGL final : public TextureCacheBase {
 public:
     explicit TextureCacheOpenGL(Core::System& system, VideoCore::RasterizerInterface& rasterizer,
-                                const Device& device);
+                                const Device& device, StateTracker& state_tracker);
     ~TextureCacheOpenGL();
 
 protected:
@@ -144,6 +145,8 @@ protected:
 private:
     GLuint FetchPBO(std::size_t buffer_size);
 
+    StateTracker& state_tracker;
+
     OGLFramebuffer src_framebuffer;
     OGLFramebuffer dst_framebuffer;
     std::unordered_map<u32, OGLBuffer> copy_pbo_cache;
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp
index 5e16bb99b..36c634e0d 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.cpp
+++ b/src/video_core/renderer_opengl/renderer_opengl.cpp
@@ -482,8 +482,8 @@ void RendererOpenGL::CreateRasterizer() {
     if (rasterizer) {
         return;
     }
-    rasterizer =
-        std::make_unique<RasterizerOpenGL>(system, emu_window, screen_info, program_manager);
+    rasterizer = std::make_unique<RasterizerOpenGL>(system, emu_window, screen_info,
+                                                    program_manager, state_tracker);
 }
 
 void RendererOpenGL::ConfigureFramebufferTexture(TextureInfo& texture,
@@ -576,6 +576,8 @@ void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) {
     glNamedBufferSubData(vertex_buffer.handle, 0, sizeof(vertices), std::data(vertices));
 
     // TODO: Signal state tracker about these changes
+    state_tracker.NotifyFramebuffer();
+
     program_manager.UseVertexShader(vertex_program.handle);
     program_manager.UseGeometryShader(0);
     program_manager.UseFragmentShader(fragment_program.handle);
diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h
index ca670e7c8..33073ce5b 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.h
+++ b/src/video_core/renderer_opengl/renderer_opengl.h
@@ -11,6 +11,7 @@
 #include "video_core/renderer_base.h"
 #include "video_core/renderer_opengl/gl_resource_manager.h"
 #include "video_core/renderer_opengl/gl_shader_manager.h"
+#include "video_core/renderer_opengl/gl_state_tracker.h"
 
 namespace Core {
 class System;
@@ -91,6 +92,8 @@ private:
     Core::Frontend::EmuWindow& emu_window;
     Core::System& system;
 
+    StateTracker state_tracker{system};
+
     // OpenGL object IDs
     OGLBuffer vertex_buffer;
     OGLProgram vertex_program;
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index ec6dfa49e..51373b687 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -22,6 +22,7 @@
 #include "core/core.h"
 #include "core/memory.h"
 #include "core/settings.h"
+#include "video_core/dirty_flags.h"
 #include "video_core/engines/fermi_2d.h"
 #include "video_core/engines/maxwell_3d.h"
 #include "video_core/gpu.h"
@@ -142,6 +143,10 @@ public:
     TView GetDepthBufferSurface(bool preserve_contents) {
         std::lock_guard lock{mutex};
         auto& maxwell3d = system.GPU().Maxwell3D();
+        if (!maxwell3d.dirty.flags[VideoCommon::Dirty::ZetaBuffer]) {
+            return depth_buffer.view;
+        }
+        maxwell3d.dirty.flags[VideoCommon::Dirty::ZetaBuffer] = false;
 
         const auto& regs{maxwell3d.regs};
         const auto gpu_addr{regs.zeta.Address()};
@@ -170,6 +175,10 @@ public:
         std::lock_guard lock{mutex};
         ASSERT(index < Tegra::Engines::Maxwell3D::Regs::NumRenderTargets);
         auto& maxwell3d = system.GPU().Maxwell3D();
+        if (!maxwell3d.dirty.flags[VideoCommon::Dirty::ColorBuffer0 + index]) {
+            return render_targets[index].view;
+        }
+        maxwell3d.dirty.flags[VideoCommon::Dirty::ColorBuffer0 + index] = false;
 
         const auto& regs{maxwell3d.regs};
         if (index >= regs.rt_control.count || regs.rt[index].Address() == 0 ||
@@ -310,7 +319,16 @@ protected:
     // and reading it from a separate buffer.
     virtual void BufferCopy(TSurface& src_surface, TSurface& dst_surface) = 0;
 
-    void ManageRenderTargetUnregister([[maybe_unused]] TSurface& surface) {}
+    void ManageRenderTargetUnregister(TSurface& surface) {
+        auto& dirty = system.GPU().Maxwell3D().dirty;
+        const u32 index = surface->GetRenderTarget();
+        if (index == DEPTH_RT) {
+            dirty.flags[VideoCommon::Dirty::ZetaBuffer] = true;
+        } else {
+            dirty.flags[VideoCommon::Dirty::ColorBuffer0 + index] = true;
+        }
+        dirty.flags[VideoCommon::Dirty::RenderTargets] = true;
+    }
 
     void Register(TSurface surface) {
         const GPUVAddr gpu_addr = surface->GetGpuAddr();