diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 18b774ac7..06c174a5b 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -199,6 +199,8 @@ if (ENABLE_VULKAN)
         renderer_vulkan/vk_shader_util.h
         renderer_vulkan/vk_staging_buffer_pool.cpp
         renderer_vulkan/vk_staging_buffer_pool.h
+        renderer_vulkan/vk_state_tracker.cpp
+        renderer_vulkan/vk_state_tracker.h
         renderer_vulkan/vk_stream_buffer.cpp
         renderer_vulkan/vk_stream_buffer.h
         renderer_vulkan/vk_swapchain.cpp
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp
index ddc62bc97..42bb01418 100644
--- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp
@@ -27,6 +27,7 @@
 #include "video_core/renderer_vulkan/vk_rasterizer.h"
 #include "video_core/renderer_vulkan/vk_resource_manager.h"
 #include "video_core/renderer_vulkan/vk_scheduler.h"
+#include "video_core/renderer_vulkan/vk_state_tracker.h"
 #include "video_core/renderer_vulkan/vk_swapchain.h"
 
 namespace Vulkan {
@@ -177,10 +178,13 @@ bool RendererVulkan::Init() {
     swapchain = std::make_unique<VKSwapchain>(surface, *device);
     swapchain->Create(framebuffer.width, framebuffer.height, false);
 
-    scheduler = std::make_unique<VKScheduler>(*device, *resource_manager);
+    state_tracker = std::make_unique<StateTracker>(system);
+
+    scheduler = std::make_unique<VKScheduler>(*device, *resource_manager, *state_tracker);
 
     rasterizer = std::make_unique<RasterizerVulkan>(system, render_window, screen_info, *device,
-                                                    *resource_manager, *memory_manager, *scheduler);
+                                                    *resource_manager, *memory_manager,
+                                                    *state_tracker, *scheduler);
 
     blit_screen = std::make_unique<VKBlitScreen>(system, render_window, *rasterizer, *device,
                                                  *resource_manager, *memory_manager, *swapchain,
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h
index f513397f0..3da08d2e4 100644
--- a/src/video_core/renderer_vulkan/renderer_vulkan.h
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.h
@@ -4,8 +4,10 @@
 
 #pragma once
 
+#include <memory>
 #include <optional>
 #include <vector>
+
 #include "video_core/renderer_base.h"
 #include "video_core/renderer_vulkan/declarations.h"
 
@@ -15,6 +17,7 @@ class System;
 
 namespace Vulkan {
 
+class StateTracker;
 class VKBlitScreen;
 class VKDevice;
 class VKFence;
@@ -61,6 +64,7 @@ private:
     std::unique_ptr<VKSwapchain> swapchain;
     std::unique_ptr<VKMemoryManager> memory_manager;
     std::unique_ptr<VKResourceManager> resource_manager;
+    std::unique_ptr<StateTracker> state_tracker;
     std::unique_ptr<VKScheduler> scheduler;
     std::unique_ptr<VKBlitScreen> blit_screen;
 };
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index b1be41a21..41cbf4134 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -36,6 +36,7 @@
 #include "video_core/renderer_vulkan/vk_sampler_cache.h"
 #include "video_core/renderer_vulkan/vk_scheduler.h"
 #include "video_core/renderer_vulkan/vk_staging_buffer_pool.h"
+#include "video_core/renderer_vulkan/vk_state_tracker.h"
 #include "video_core/renderer_vulkan/vk_texture_cache.h"
 #include "video_core/renderer_vulkan/vk_update_descriptor.h"
 
@@ -277,10 +278,11 @@ void RasterizerVulkan::DrawParameters::Draw(vk::CommandBuffer cmdbuf,
 RasterizerVulkan::RasterizerVulkan(Core::System& system, Core::Frontend::EmuWindow& renderer,
                                    VKScreenInfo& screen_info, const VKDevice& device,
                                    VKResourceManager& resource_manager,
-                                   VKMemoryManager& memory_manager, VKScheduler& scheduler)
+                                   VKMemoryManager& memory_manager, StateTracker& state_tracker,
+                                   VKScheduler& scheduler)
     : RasterizerAccelerated{system.Memory()}, system{system}, render_window{renderer},
       screen_info{screen_info}, device{device}, resource_manager{resource_manager},
-      memory_manager{memory_manager}, scheduler{scheduler},
+      memory_manager{memory_manager}, state_tracker{state_tracker}, scheduler{scheduler},
       staging_pool(device, memory_manager, scheduler), descriptor_pool(device),
       update_descriptor_queue(device, scheduler),
       quad_array_pass(device, scheduler, descriptor_pool, staging_pool, update_descriptor_queue),
@@ -545,6 +547,10 @@ bool RasterizerVulkan::AccelerateDisplay(const Tegra::FramebufferConfig& config,
     return true;
 }
 
+void RasterizerVulkan::SetupDirtyFlags() {
+    state_tracker.Initialize();
+}
+
 void RasterizerVulkan::FlushWork() {
     static constexpr u32 DRAWS_TO_DISPATCH = 4096;
 
@@ -568,7 +574,9 @@ void RasterizerVulkan::FlushWork() {
 
 RasterizerVulkan::Texceptions RasterizerVulkan::UpdateAttachments() {
     MICROPROFILE_SCOPE(Vulkan_RenderTargets);
-    constexpr bool update_rendertargets = true;
+    auto& dirty = system.GPU().Maxwell3D().dirty.flags;
+    const bool update_rendertargets = dirty[VideoCommon::Dirty::RenderTargets];
+    dirty[VideoCommon::Dirty::RenderTargets] = false;
 
     texture_cache.GuardRenderTargets(true);
 
@@ -971,6 +979,9 @@ void RasterizerVulkan::SetupImage(const Tegra::Texture::TICEntry& tic, const Ima
 }
 
 void RasterizerVulkan::UpdateViewportsState(Tegra::Engines::Maxwell3D& gpu) {
+    if (!state_tracker.TouchViewports()) {
+        return;
+    }
     const auto& regs = gpu.regs;
     const std::array viewports{
         GetViewportState(device, regs, 0),  GetViewportState(device, regs, 1),
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h
index 4dc8af6e8..a79440eba 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.h
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.h
@@ -96,6 +96,7 @@ struct hash<Vulkan::FramebufferCacheKey> {
 
 namespace Vulkan {
 
+class StateTracker;
 class BufferBindings;
 
 struct ImageView {
@@ -108,7 +109,7 @@ public:
     explicit RasterizerVulkan(Core::System& system, Core::Frontend::EmuWindow& render_window,
                               VKScreenInfo& screen_info, const VKDevice& device,
                               VKResourceManager& resource_manager, VKMemoryManager& memory_manager,
-                              VKScheduler& scheduler);
+                              StateTracker& state_tracker, VKScheduler& scheduler);
     ~RasterizerVulkan() override;
 
     void Draw(bool is_indexed, bool is_instanced) override;
@@ -127,6 +128,7 @@ public:
                                const Tegra::Engines::Fermi2D::Config& copy_config) override;
     bool AccelerateDisplay(const Tegra::FramebufferConfig& config, VAddr framebuffer_addr,
                            u32 pixel_stride) override;
+    void SetupDirtyFlags() override;
 
     /// Maximum supported size that a constbuffer can have in bytes.
     static constexpr std::size_t MaxConstbufferSize = 0x10000;
@@ -241,6 +243,7 @@ private:
     const VKDevice& device;
     VKResourceManager& resource_manager;
     VKMemoryManager& memory_manager;
+    StateTracker& state_tracker;
     VKScheduler& scheduler;
 
     VKStagingBufferPool staging_pool;
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp
index 92bd6c344..b61d4fe63 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.cpp
+++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp
@@ -2,6 +2,12 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <thread>
+#include <utility>
+
 #include "common/assert.h"
 #include "common/microprofile.h"
 #include "video_core/renderer_vulkan/declarations.h"
@@ -9,6 +15,7 @@
 #include "video_core/renderer_vulkan/vk_query_cache.h"
 #include "video_core/renderer_vulkan/vk_resource_manager.h"
 #include "video_core/renderer_vulkan/vk_scheduler.h"
+#include "video_core/renderer_vulkan/vk_state_tracker.h"
 
 namespace Vulkan {
 
@@ -29,9 +36,10 @@ void VKScheduler::CommandChunk::ExecuteAll(vk::CommandBuffer cmdbuf,
     last = nullptr;
 }
 
-VKScheduler::VKScheduler(const VKDevice& device, VKResourceManager& resource_manager)
-    : device{device}, resource_manager{resource_manager}, next_fence{
-                                                              &resource_manager.CommitFence()} {
+VKScheduler::VKScheduler(const VKDevice& device, VKResourceManager& resource_manager,
+                         StateTracker& state_tracker)
+    : device{device}, resource_manager{resource_manager}, state_tracker{state_tracker},
+      next_fence{&resource_manager.CommitFence()} {
     AcquireNewChunk();
     AllocateNewContext();
     worker_thread = std::thread(&VKScheduler::WorkerThread, this);
@@ -157,12 +165,7 @@ void VKScheduler::AllocateNewContext() {
 
 void VKScheduler::InvalidateState() {
     state.graphics_pipeline = nullptr;
-    state.viewports = false;
-    state.scissors = false;
-    state.depth_bias = false;
-    state.blend_constants = false;
-    state.depth_bounds = false;
-    state.stencil_values = false;
+    state_tracker.InvalidateCommandBufferState();
 }
 
 void VKScheduler::EndPendingOperations() {
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h
index 62fd7858b..c7cc291c3 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.h
+++ b/src/video_core/renderer_vulkan/vk_scheduler.h
@@ -17,6 +17,7 @@
 
 namespace Vulkan {
 
+class StateTracker;
 class VKDevice;
 class VKFence;
 class VKQueryCache;
@@ -43,7 +44,8 @@ private:
 /// OpenGL-like operations on Vulkan command buffers.
 class VKScheduler {
 public:
-    explicit VKScheduler(const VKDevice& device, VKResourceManager& resource_manager);
+    explicit VKScheduler(const VKDevice& device, VKResourceManager& resource_manager,
+                         StateTracker& state_tracker);
     ~VKScheduler();
 
     /// Sends the current execution context to the GPU.
@@ -74,36 +76,6 @@ public:
         query_cache = &query_cache_;
     }
 
-    /// Returns true when viewports have been set in the current command buffer.
-    bool TouchViewports() {
-        return std::exchange(state.viewports, true);
-    }
-
-    /// Returns true when scissors have been set in the current command buffer.
-    bool TouchScissors() {
-        return std::exchange(state.scissors, true);
-    }
-
-    /// Returns true when depth bias have been set in the current command buffer.
-    bool TouchDepthBias() {
-        return std::exchange(state.depth_bias, true);
-    }
-
-    /// Returns true when blend constants have been set in the current command buffer.
-    bool TouchBlendConstants() {
-        return std::exchange(state.blend_constants, true);
-    }
-
-    /// Returns true when depth bounds have been set in the current command buffer.
-    bool TouchDepthBounds() {
-        return std::exchange(state.depth_bounds, true);
-    }
-
-    /// Returns true when stencil values have been set in the current command buffer.
-    bool TouchStencilValues() {
-        return std::exchange(state.stencil_values, true);
-    }
-
     /// Send work to a separate thread.
     template <typename T>
     void Record(T&& command) {
@@ -217,6 +189,8 @@ private:
 
     const VKDevice& device;
     VKResourceManager& resource_manager;
+    StateTracker& state_tracker;
+
     VKQueryCache* query_cache = nullptr;
 
     vk::CommandBuffer current_cmdbuf;
@@ -226,12 +200,6 @@ private:
     struct State {
         std::optional<vk::RenderPassBeginInfo> renderpass;
         vk::Pipeline graphics_pipeline;
-        bool viewports = false;
-        bool scissors = false;
-        bool depth_bias = false;
-        bool blend_constants = false;
-        bool depth_bounds = false;
-        bool stencil_values = false;
     } state;
 
     std::unique_ptr<CommandChunk> chunk;
diff --git a/src/video_core/renderer_vulkan/vk_state_tracker.cpp b/src/video_core/renderer_vulkan/vk_state_tracker.cpp
new file mode 100644
index 000000000..d44992dc9
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_state_tracker.cpp
@@ -0,0 +1,97 @@
+// Copyright 2020 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#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_vulkan/vk_state_tracker.h"
+
+#define OFF(field_name) MAXWELL3D_REG_INDEX(field_name)
+#define NUM(field_name) (sizeof(Maxwell3D::Regs::field_name) / sizeof(u32))
+
+namespace Vulkan {
+
+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])>;
+using Flags = std::remove_reference_t<decltype(Maxwell3D::dirty.flags)>;
+
+Flags MakeInvalidationFlags() {
+    Flags flags{};
+    flags[Viewports] = true;
+    return flags;
+}
+
+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);
+    }
+}
+
+void SetupDirtyViewports(Tables& tables) {
+    FillBlock(tables[0], OFF(viewport_transform), NUM(viewport_transform), Viewports);
+    FillBlock(tables[0], OFF(viewports), NUM(viewports), Viewports);
+    tables[0][OFF(viewport_transform_enabled)] = Viewports;
+}
+
+} // Anonymous namespace
+
+StateTracker::StateTracker(Core::System& system)
+    : system{system}, invalidation_flags{MakeInvalidationFlags()} {}
+
+void StateTracker::Initialize() {
+    auto& dirty = system.GPU().Maxwell3D().dirty;
+    auto& tables = dirty.tables;
+    SetupDirtyRenderTargets(tables);
+    SetupDirtyViewports(tables);
+
+    auto& store = dirty.on_write_stores;
+    store[RenderTargets] = true;
+    store[ZetaBuffer] = true;
+    for (std::size_t i = 0; i < Regs::NumRenderTargets; ++i) {
+        store[ColorBuffer0 + i] = true;
+    }
+}
+
+void StateTracker::InvalidateCommandBufferState() {
+    system.GPU().Maxwell3D().dirty.flags |= invalidation_flags;
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_state_tracker.h b/src/video_core/renderer_vulkan/vk_state_tracker.h
new file mode 100644
index 000000000..9ec7b5136
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_state_tracker.h
@@ -0,0 +1,53 @@
+// Copyright 2020 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <type_traits> // REMOVE ME
+#include <utility>
+
+#include "common/common_types.h"
+#include "core/core.h"
+#include "video_core/dirty_flags.h"
+#include "video_core/engines/maxwell_3d.h"
+
+namespace Vulkan {
+
+namespace Dirty {
+
+enum : u8 {
+    First = VideoCommon::Dirty::LastCommonEntry,
+
+    Viewports,
+};
+
+} // namespace Dirty
+
+class StateTracker {
+public:
+    explicit StateTracker(Core::System& system);
+
+    void Initialize();
+
+    void InvalidateCommandBufferState();
+
+    bool TouchViewports() {
+        return Exchange(Dirty::Viewports, false);
+    }
+
+private:
+    using Flags = std::remove_reference_t<decltype(Tegra::Engines::Maxwell3D::dirty.flags)>;
+
+    bool Exchange(std::size_t id, bool new_value) const noexcept {
+        auto& flags = system.GPU().Maxwell3D().dirty.flags;
+        const bool is_dirty = flags[id];
+        flags[id] = new_value;
+        return is_dirty;
+    }
+
+    Core::System& system;
+    Flags invalidation_flags;
+};
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 51b0d38a6..73d92a5ae 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -22,6 +22,7 @@
 #include "video_core/renderer_vulkan/vk_device.h"
 #include "video_core/renderer_vulkan/vk_memory_manager.h"
 #include "video_core/renderer_vulkan/vk_rasterizer.h"
+#include "video_core/renderer_vulkan/vk_scheduler.h"
 #include "video_core/renderer_vulkan/vk_staging_buffer_pool.h"
 #include "video_core/renderer_vulkan/vk_texture_cache.h"
 #include "video_core/surface.h"