diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index bef0d7a1e9..242c2db0ca 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -130,6 +130,10 @@ add_library(core STATIC
     hle/service/nvdrv/nvdrv.h
     hle/service/nvdrv/nvmemp.cpp
     hle/service/nvdrv/nvmemp.h
+    hle/service/nvflinger/buffer_queue.cpp
+    hle/service/nvflinger/buffer_queue.h
+    hle/service/nvflinger/nvflinger.cpp
+    hle/service/nvflinger/nvflinger.h
     hle/service/pctl/pctl.cpp
     hle/service/pctl/pctl.h
     hle/service/pctl/pctl_a.cpp
diff --git a/src/core/hle/service/nvflinger/buffer_queue.cpp b/src/core/hle/service/nvflinger/buffer_queue.cpp
new file mode 100644
index 0000000000..705bdbe5d4
--- /dev/null
+++ b/src/core/hle/service/nvflinger/buffer_queue.cpp
@@ -0,0 +1,96 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+
+#include "common/alignment.h"
+#include "common/scope_exit.h"
+#include "core/core_timing.h"
+#include "core/hle/service/nvflinger/buffer_queue.h"
+
+namespace Service {
+namespace NVFlinger {
+
+BufferQueue::BufferQueue(u32 id, u64 layer_id) : id(id), layer_id(layer_id) {
+    native_handle = Kernel::Event::Create(Kernel::ResetType::OneShot, "BufferQueue NativeHandle");
+}
+
+void BufferQueue::SetPreallocatedBuffer(u32 slot, IGBPBuffer& igbp_buffer) {
+    Buffer buffer{};
+    buffer.slot = slot;
+    buffer.igbp_buffer = igbp_buffer;
+    buffer.status = Buffer::Status::Free;
+
+    LOG_WARNING(Service, "Adding graphics buffer %u", slot);
+
+    queue.emplace_back(buffer);
+}
+
+u32 BufferQueue::DequeueBuffer(u32 pixel_format, u32 width, u32 height) {
+    auto itr = std::find_if(queue.begin(), queue.end(), [&](const Buffer& buffer) {
+        // Only consider free buffers. Buffers become free once again after they've been Acquired
+        // and Released by the compositor, see the NVFlinger::Compose method.
+        if (buffer.status != Buffer::Status::Free)
+            return false;
+
+        // Make sure that the parameters match.
+        auto& igbp_buffer = buffer.igbp_buffer;
+        return igbp_buffer.format == pixel_format && igbp_buffer.width == width &&
+               igbp_buffer.height == height;
+    });
+    ASSERT(itr != queue.end());
+
+    itr->status = Buffer::Status::Dequeued;
+    return itr->slot;
+}
+
+const IGBPBuffer& BufferQueue::RequestBuffer(u32 slot) const {
+    auto itr = std::find_if(queue.begin(), queue.end(),
+                            [&](const Buffer& buffer) { return buffer.slot == slot; });
+    ASSERT(itr != queue.end());
+    ASSERT(itr->status == Buffer::Status::Dequeued);
+    return itr->igbp_buffer;
+}
+
+void BufferQueue::QueueBuffer(u32 slot) {
+    auto itr = std::find_if(queue.begin(), queue.end(),
+                            [&](const Buffer& buffer) { return buffer.slot == slot; });
+    ASSERT(itr != queue.end());
+    ASSERT(itr->status == Buffer::Status::Dequeued);
+    itr->status = Buffer::Status::Queued;
+}
+
+boost::optional<const BufferQueue::Buffer&> BufferQueue::AcquireBuffer() {
+    auto itr = std::find_if(queue.begin(), queue.end(), [](const Buffer& buffer) {
+        return buffer.status == Buffer::Status::Queued;
+    });
+    if (itr == queue.end())
+        return boost::none;
+    itr->status = Buffer::Status::Acquired;
+    return *itr;
+}
+
+void BufferQueue::ReleaseBuffer(u32 slot) {
+    auto itr = std::find_if(queue.begin(), queue.end(),
+                            [&](const Buffer& buffer) { return buffer.slot == slot; });
+    ASSERT(itr != queue.end());
+    ASSERT(itr->status == Buffer::Status::Acquired);
+    itr->status = Buffer::Status::Free;
+}
+
+u32 BufferQueue::Query(QueryType type) {
+    LOG_WARNING(Service, "(STUBBED) called type=%u", static_cast<u32>(type));
+    switch (type) {
+    case QueryType::NativeWindowFormat:
+        // TODO(Subv): Use an enum for this
+        static constexpr u32 FormatABGR8 = 1;
+        return FormatABGR8;
+    }
+
+    UNIMPLEMENTED();
+    return 0;
+}
+
+} // namespace NVFlinger
+} // namespace Service
diff --git a/src/core/hle/service/nvflinger/buffer_queue.h b/src/core/hle/service/nvflinger/buffer_queue.h
new file mode 100644
index 0000000000..5c6719407f
--- /dev/null
+++ b/src/core/hle/service/nvflinger/buffer_queue.h
@@ -0,0 +1,82 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <vector>
+#include <boost/optional.hpp>
+#include "common/swap.h"
+#include "core/hle/kernel/event.h"
+
+namespace CoreTiming {
+struct EventType;
+}
+
+namespace Service {
+namespace NVFlinger {
+
+struct IGBPBuffer {
+    u32_le magic;
+    u32_le width;
+    u32_le height;
+    u32_le stride;
+    u32_le format;
+    u32_le usage;
+    INSERT_PADDING_WORDS(1);
+    u32_le index;
+    INSERT_PADDING_WORDS(3);
+    u32_le gpu_buffer_id;
+    INSERT_PADDING_WORDS(17);
+    u32_le nvmap_handle;
+    u32_le offset;
+    INSERT_PADDING_WORDS(60);
+};
+
+static_assert(sizeof(IGBPBuffer) == 0x16C, "IGBPBuffer has wrong size");
+
+class BufferQueue final {
+public:
+    enum class QueryType {
+        NativeWindowWidth = 0,
+        NativeWindowHeight = 1,
+        NativeWindowFormat = 2,
+    };
+
+    BufferQueue(u32 id, u64 layer_id);
+    ~BufferQueue() = default;
+
+    struct Buffer {
+        enum class Status { Free = 0, Queued = 1, Dequeued = 2, Acquired = 3 };
+
+        u32 slot;
+        Status status = Status::Free;
+        IGBPBuffer igbp_buffer;
+    };
+
+    void SetPreallocatedBuffer(u32 slot, IGBPBuffer& buffer);
+    u32 DequeueBuffer(u32 pixel_format, u32 width, u32 height);
+    const IGBPBuffer& RequestBuffer(u32 slot) const;
+    void QueueBuffer(u32 slot);
+    boost::optional<const Buffer&> AcquireBuffer();
+    void ReleaseBuffer(u32 slot);
+    u32 Query(QueryType type);
+
+    u32 GetId() const {
+        return id;
+    }
+
+    Kernel::SharedPtr<Kernel::Event> GetNativeHandle() const {
+        return native_handle;
+    }
+
+private:
+    u32 id;
+    u64 layer_id;
+
+    std::vector<Buffer> queue;
+    Kernel::SharedPtr<Kernel::Event> native_handle;
+};
+
+} // namespace NVFlinger
+} // namespace Service
diff --git a/src/core/hle/service/nvflinger/nvflinger.cpp b/src/core/hle/service/nvflinger/nvflinger.cpp
new file mode 100644
index 0000000000..fe622b9864
--- /dev/null
+++ b/src/core/hle/service/nvflinger/nvflinger.cpp
@@ -0,0 +1,161 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+
+#include "common/alignment.h"
+#include "common/scope_exit.h"
+#include "core/core_timing.h"
+#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
+#include "core/hle/service/nvdrv/nvdrv.h"
+#include "core/hle/service/nvflinger/buffer_queue.h"
+#include "core/hle/service/nvflinger/nvflinger.h"
+#include "video_core/renderer_base.h"
+#include "video_core/video_core.h"
+
+namespace Service {
+namespace NVFlinger {
+
+constexpr size_t SCREEN_REFRESH_RATE = 60;
+constexpr u64 frame_ticks = static_cast<u64>(BASE_CLOCK_RATE / SCREEN_REFRESH_RATE);
+
+NVFlinger::NVFlinger() {
+    // Add the different displays to the list of displays.
+    Display default_{0, "Default"};
+    Display external{1, "External"};
+    Display edid{2, "Edid"};
+    Display internal{3, "Internal"};
+
+    displays.emplace_back(default_);
+    displays.emplace_back(external);
+    displays.emplace_back(edid);
+    displays.emplace_back(internal);
+
+    // Schedule the screen composition events
+    composition_event =
+        CoreTiming::RegisterEvent("ScreenCompositioin", [this](u64 userdata, int cycles_late) {
+            Compose();
+            CoreTiming::ScheduleEvent(frame_ticks - cycles_late, composition_event);
+        });
+
+    CoreTiming::ScheduleEvent(frame_ticks, composition_event);
+}
+
+NVFlinger::~NVFlinger() {
+    CoreTiming::UnscheduleEvent(composition_event, 0);
+}
+
+u64 NVFlinger::OpenDisplay(const std::string& name) {
+    LOG_WARNING(Service, "Opening display %s", name.c_str());
+
+    // TODO(Subv): Currently we only support the Default display.
+    ASSERT(name == "Default");
+
+    auto itr = std::find_if(displays.begin(), displays.end(),
+                            [&](const Display& display) { return display.name == name; });
+
+    ASSERT(itr != displays.end());
+
+    return itr->id;
+}
+
+u64 NVFlinger::CreateLayer(u64 display_id) {
+    auto& display = GetDisplay(display_id);
+
+    ASSERT_MSG(display.layers.empty(), "Only one layer is supported per display at the moment");
+
+    u64 layer_id = next_layer_id++;
+    u32 buffer_queue_id = next_buffer_queue_id++;
+    auto buffer_queue = std::make_shared<BufferQueue>(buffer_queue_id, layer_id);
+    display.layers.emplace_back(layer_id, buffer_queue);
+    buffer_queues.emplace_back(std::move(buffer_queue));
+    return layer_id;
+}
+
+u32 NVFlinger::GetBufferQueueId(u64 display_id, u64 layer_id) {
+    const auto& layer = GetLayer(display_id, layer_id);
+    return layer.buffer_queue->GetId();
+}
+
+Kernel::SharedPtr<Kernel::Event> NVFlinger::GetVsyncEvent(u64 display_id) {
+    const auto& display = GetDisplay(display_id);
+    return display.vsync_event;
+}
+
+std::shared_ptr<BufferQueue> NVFlinger::GetBufferQueue(u32 id) const {
+    auto itr = std::find_if(buffer_queues.begin(), buffer_queues.end(),
+                            [&](const auto& queue) { return queue->GetId() == id; });
+
+    ASSERT(itr != buffer_queues.end());
+    return *itr;
+}
+
+Display& NVFlinger::GetDisplay(u64 display_id) {
+    auto itr = std::find_if(displays.begin(), displays.end(),
+                            [&](const Display& display) { return display.id == display_id; });
+
+    ASSERT(itr != displays.end());
+    return *itr;
+}
+
+Layer& NVFlinger::GetLayer(u64 display_id, u64 layer_id) {
+    auto& display = GetDisplay(display_id);
+
+    auto itr = std::find_if(display.layers.begin(), display.layers.end(),
+                            [&](const Layer& layer) { return layer.id == layer_id; });
+
+    ASSERT(itr != display.layers.end());
+    return *itr;
+}
+
+void NVFlinger::Compose() {
+    for (auto& display : displays) {
+        // Trigger vsync for this display at the end of drawing
+        SCOPE_EXIT({ display.vsync_event->Signal(); });
+
+        // Don't do anything for displays without layers.
+        if (display.layers.empty())
+            continue;
+
+        // TODO(Subv): Support more than 1 layer.
+        ASSERT_MSG(display.layers.size() == 1, "Max 1 layer per display is supported");
+
+        Layer& layer = display.layers[0];
+        auto& buffer_queue = layer.buffer_queue;
+
+        // Search for a queued buffer and acquire it
+        auto buffer = buffer_queue->AcquireBuffer();
+
+        if (buffer == boost::none) {
+            // There was no queued buffer to draw, render previous frame
+            VideoCore::g_renderer->SwapBuffers({});
+            continue;
+        }
+
+        auto& igbp_buffer = buffer->igbp_buffer;
+
+        // Now send the buffer to the GPU for drawing.
+        auto nvdrv = Nvidia::nvdrv.lock();
+        ASSERT(nvdrv);
+
+        // TODO(Subv): Support more than just disp0. The display device selection is probably based
+        // on which display we're drawing (Default, Internal, External, etc)
+        auto nvdisp = nvdrv->GetDevice<Nvidia::Devices::nvdisp_disp0>("/dev/nvdisp_disp0");
+        ASSERT(nvdisp);
+
+        nvdisp->flip(igbp_buffer.gpu_buffer_id, igbp_buffer.offset, igbp_buffer.format,
+                     igbp_buffer.width, igbp_buffer.height, igbp_buffer.stride);
+
+        buffer_queue->ReleaseBuffer(buffer->slot);
+    }
+}
+
+Layer::Layer(u64 id, std::shared_ptr<BufferQueue> queue) : id(id), buffer_queue(std::move(queue)) {}
+
+Display::Display(u64 id, std::string name) : id(id), name(std::move(name)) {
+    vsync_event = Kernel::Event::Create(Kernel::ResetType::Pulse, "Display VSync Event");
+}
+
+} // namespace NVFlinger
+} // namespace Service
diff --git a/src/core/hle/service/nvflinger/nvflinger.h b/src/core/hle/service/nvflinger/nvflinger.h
new file mode 100644
index 0000000000..3126018adb
--- /dev/null
+++ b/src/core/hle/service/nvflinger/nvflinger.h
@@ -0,0 +1,84 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <boost/optional.hpp>
+#include "core/hle/kernel/event.h"
+
+namespace CoreTiming {
+struct EventType;
+}
+
+namespace Service {
+namespace NVFlinger {
+
+class BufferQueue;
+
+struct Layer {
+    Layer(u64 id, std::shared_ptr<BufferQueue> queue);
+    ~Layer() = default;
+
+    u64 id;
+    std::shared_ptr<BufferQueue> buffer_queue;
+};
+
+struct Display {
+    Display(u64 id, std::string name);
+    ~Display() = default;
+
+    u64 id;
+    std::string name;
+
+    std::vector<Layer> layers;
+    Kernel::SharedPtr<Kernel::Event> vsync_event;
+};
+
+class NVFlinger final {
+public:
+    NVFlinger();
+    ~NVFlinger();
+
+    /// Opens the specified display and returns the id.
+    u64 OpenDisplay(const std::string& name);
+
+    /// Creates a layer on the specified display and returns the layer id.
+    u64 CreateLayer(u64 display_id);
+
+    /// Gets the buffer queue id of the specified layer in the specified display.
+    u32 GetBufferQueueId(u64 display_id, u64 layer_id);
+
+    /// Gets the vsync event for the specified display.
+    Kernel::SharedPtr<Kernel::Event> GetVsyncEvent(u64 display_id);
+
+    /// Obtains a buffer queue identified by the id.
+    std::shared_ptr<BufferQueue> GetBufferQueue(u32 id) const;
+
+    /// Performs a composition request to the emulated nvidia GPU and triggers the vsync events when
+    /// finished.
+    void Compose();
+
+private:
+    /// Returns the display identified by the specified id.
+    Display& GetDisplay(u64 display_id);
+
+    /// Returns the layer identified by the specified id in the desired display.
+    Layer& GetLayer(u64 display_id, u64 layer_id);
+
+    std::vector<Display> displays;
+    std::vector<std::shared_ptr<BufferQueue>> buffer_queues;
+
+    /// Id to use for the next layer that is created, this counter is shared among all displays.
+    u64 next_layer_id = 1;
+    /// Id to use for the next buffer queue that is created, this counter is shared among all
+    /// layers.
+    u32 next_buffer_queue_id = 1;
+
+    /// CoreTiming event that handles screen composition.
+    CoreTiming::EventType* composition_event;
+};
+
+} // namespace NVFlinger
+} // namespace Service
diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp
index 3f2fd72b2d..e0bfad2909 100644
--- a/src/core/hle/service/vi/vi.cpp
+++ b/src/core/hle/service/vi/vi.cpp
@@ -8,8 +8,7 @@
 #include "common/scope_exit.h"
 #include "core/core_timing.h"
 #include "core/hle/ipc_helpers.h"
-#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
-#include "core/hle/service/nvdrv/nvdrv.h"
+#include "core/hle/service/nvflinger/buffer_queue.h"
 #include "core/hle/service/vi/vi.h"
 #include "core/hle/service/vi/vi_m.h"
 #include "video_core/renderer_base.h"
@@ -18,9 +17,6 @@
 namespace Service {
 namespace VI {
 
-constexpr size_t SCREEN_REFRESH_RATE = 60;
-constexpr u64 frame_ticks = static_cast<u64>(BASE_CLOCK_RATE / SCREEN_REFRESH_RATE);
-
 class Parcel {
 public:
     // This default size was chosen arbitrarily.
@@ -204,8 +200,8 @@ public:
     void DeserializeData() override {
         std::u16string token = ReadInterfaceToken();
         data = Read<Data>();
-        ASSERT(data.graphic_buffer_length == sizeof(IGBPBuffer));
-        buffer = Read<IGBPBuffer>();
+        ASSERT(data.graphic_buffer_length == sizeof(NVFlinger::IGBPBuffer));
+        buffer = Read<NVFlinger::IGBPBuffer>();
     }
 
     struct Data {
@@ -216,7 +212,7 @@ public:
     };
 
     Data data;
-    IGBPBuffer buffer;
+    NVFlinger::IGBPBuffer buffer;
 };
 
 class IGBPSetPreallocatedBufferResponseParcel : public Parcel {
@@ -288,7 +284,8 @@ public:
 
 class IGBPRequestBufferResponseParcel : public Parcel {
 public:
-    explicit IGBPRequestBufferResponseParcel(IGBPBuffer buffer) : Parcel(), buffer(buffer) {}
+    explicit IGBPRequestBufferResponseParcel(NVFlinger::IGBPBuffer buffer)
+        : Parcel(), buffer(buffer) {}
     ~IGBPRequestBufferResponseParcel() override = default;
 
 protected:
@@ -296,7 +293,7 @@ protected:
         // TODO(Subv): Find out what this all means
         Write<u32_le>(1);
 
-        Write<u32_le>(sizeof(IGBPBuffer));
+        Write<u32_le>(sizeof(NVFlinger::IGBPBuffer));
         Write<u32_le>(0); // Unknown
 
         Write(buffer);
@@ -304,7 +301,7 @@ protected:
         Write<u32_le>(0);
     }
 
-    IGBPBuffer buffer;
+    NVFlinger::IGBPBuffer buffer;
 };
 
 class IGBPQueueBufferRequestParcel : public Parcel {
@@ -387,7 +384,7 @@ private:
 
 class IHOSBinderDriver final : public ServiceFramework<IHOSBinderDriver> {
 public:
-    explicit IHOSBinderDriver(std::shared_ptr<NVFlinger> nv_flinger)
+    explicit IHOSBinderDriver(std::shared_ptr<NVFlinger::NVFlinger> nv_flinger)
         : ServiceFramework("IHOSBinderDriver"), nv_flinger(std::move(nv_flinger)) {
         static const FunctionInfo functions[] = {
             {0, &IHOSBinderDriver::TransactParcel, "TransactParcel"},
@@ -477,7 +474,8 @@ private:
         } else if (transaction == TransactionId::Query) {
             IGBPQueryRequestParcel request{input_data};
 
-            u32 value = buffer_queue->Query(static_cast<BufferQueue::QueryType>(request.type));
+            u32 value =
+                buffer_queue->Query(static_cast<NVFlinger::BufferQueue::QueryType>(request.type));
 
             IGBPQueryResponseParcel response{value};
             auto response_buffer = response.Serialize();
@@ -518,7 +516,7 @@ private:
         rb.PushCopyObjects(buffer_queue->GetNativeHandle());
     }
 
-    std::shared_ptr<NVFlinger> nv_flinger;
+    std::shared_ptr<NVFlinger::NVFlinger> nv_flinger;
 };
 
 class ISystemDisplayService final : public ServiceFramework<ISystemDisplayService> {
@@ -546,7 +544,7 @@ private:
 
 class IManagerDisplayService final : public ServiceFramework<IManagerDisplayService> {
 public:
-    explicit IManagerDisplayService(std::shared_ptr<NVFlinger> nv_flinger)
+    explicit IManagerDisplayService(std::shared_ptr<NVFlinger::NVFlinger> nv_flinger)
         : ServiceFramework("IManagerDisplayService"), nv_flinger(std::move(nv_flinger)) {
         static const FunctionInfo functions[] = {
             {1020, &IManagerDisplayService::CloseDisplay, "CloseDisplay"},
@@ -593,7 +591,7 @@ private:
         rb.Push(RESULT_SUCCESS);
     }
 
-    std::shared_ptr<NVFlinger> nv_flinger;
+    std::shared_ptr<NVFlinger::NVFlinger> nv_flinger;
 };
 
 void IApplicationDisplayService::GetRelayService(Kernel::HLERequestContext& ctx) {
@@ -734,7 +732,8 @@ void IApplicationDisplayService::GetDisplayVsyncEvent(Kernel::HLERequestContext&
     rb.PushCopyObjects(vsync_event);
 }
 
-IApplicationDisplayService::IApplicationDisplayService(std::shared_ptr<NVFlinger> nv_flinger)
+IApplicationDisplayService::IApplicationDisplayService(
+    std::shared_ptr<NVFlinger::NVFlinger> nv_flinger)
     : ServiceFramework("IApplicationDisplayService"), nv_flinger(std::move(nv_flinger)) {
     static const FunctionInfo functions[] = {
         {100, &IApplicationDisplayService::GetRelayService, "GetRelayService"},
@@ -758,222 +757,5 @@ void InstallInterfaces(SM::ServiceManager& service_manager) {
     std::make_shared<VI_M>()->InstallAsService(service_manager);
 }
 
-NVFlinger::NVFlinger() {
-    // Add the different displays to the list of displays.
-    Display default_{0, "Default"};
-    Display external{1, "External"};
-    Display edid{2, "Edid"};
-    Display internal{3, "Internal"};
-
-    displays.emplace_back(default_);
-    displays.emplace_back(external);
-    displays.emplace_back(edid);
-    displays.emplace_back(internal);
-
-    // Schedule the screen composition events
-    composition_event =
-        CoreTiming::RegisterEvent("ScreenCompositioin", [this](u64 userdata, int cycles_late) {
-            Compose();
-            CoreTiming::ScheduleEvent(frame_ticks - cycles_late, composition_event);
-        });
-
-    CoreTiming::ScheduleEvent(frame_ticks, composition_event);
-}
-
-NVFlinger::~NVFlinger() {
-    CoreTiming::UnscheduleEvent(composition_event, 0);
-}
-
-u64 NVFlinger::OpenDisplay(const std::string& name) {
-    LOG_WARNING(Service, "Opening display %s", name.c_str());
-
-    // TODO(Subv): Currently we only support the Default display.
-    ASSERT(name == "Default");
-
-    auto itr = std::find_if(displays.begin(), displays.end(),
-                            [&](const Display& display) { return display.name == name; });
-
-    ASSERT(itr != displays.end());
-
-    return itr->id;
-}
-
-u64 NVFlinger::CreateLayer(u64 display_id) {
-    auto& display = GetDisplay(display_id);
-
-    ASSERT_MSG(display.layers.empty(), "Only one layer is supported per display at the moment");
-
-    u64 layer_id = next_layer_id++;
-    u32 buffer_queue_id = next_buffer_queue_id++;
-    auto buffer_queue = std::make_shared<BufferQueue>(buffer_queue_id, layer_id);
-    display.layers.emplace_back(layer_id, buffer_queue);
-    buffer_queues.emplace_back(std::move(buffer_queue));
-    return layer_id;
-}
-
-u32 NVFlinger::GetBufferQueueId(u64 display_id, u64 layer_id) {
-    const auto& layer = GetLayer(display_id, layer_id);
-    return layer.buffer_queue->GetId();
-}
-
-Kernel::SharedPtr<Kernel::Event> NVFlinger::GetVsyncEvent(u64 display_id) {
-    const auto& display = GetDisplay(display_id);
-    return display.vsync_event;
-}
-
-std::shared_ptr<BufferQueue> NVFlinger::GetBufferQueue(u32 id) const {
-    auto itr = std::find_if(buffer_queues.begin(), buffer_queues.end(),
-                            [&](const auto& queue) { return queue->GetId() == id; });
-
-    ASSERT(itr != buffer_queues.end());
-    return *itr;
-}
-
-Display& NVFlinger::GetDisplay(u64 display_id) {
-    auto itr = std::find_if(displays.begin(), displays.end(),
-                            [&](const Display& display) { return display.id == display_id; });
-
-    ASSERT(itr != displays.end());
-    return *itr;
-}
-
-Layer& NVFlinger::GetLayer(u64 display_id, u64 layer_id) {
-    auto& display = GetDisplay(display_id);
-
-    auto itr = std::find_if(display.layers.begin(), display.layers.end(),
-                            [&](const Layer& layer) { return layer.id == layer_id; });
-
-    ASSERT(itr != display.layers.end());
-    return *itr;
-}
-
-void NVFlinger::Compose() {
-    for (auto& display : displays) {
-        // Trigger vsync for this display at the end of drawing
-        SCOPE_EXIT({ display.vsync_event->Signal(); });
-
-        // Don't do anything for displays without layers.
-        if (display.layers.empty())
-            continue;
-
-        // TODO(Subv): Support more than 1 layer.
-        ASSERT_MSG(display.layers.size() == 1, "Max 1 layer per display is supported");
-
-        Layer& layer = display.layers[0];
-        auto& buffer_queue = layer.buffer_queue;
-
-        // Search for a queued buffer and acquire it
-        auto buffer = buffer_queue->AcquireBuffer();
-
-        if (buffer == boost::none) {
-            // There was no queued buffer to draw, render previous frame
-            VideoCore::g_renderer->SwapBuffers({});
-            continue;
-        }
-
-        auto& igbp_buffer = buffer->igbp_buffer;
-
-        // Now send the buffer to the GPU for drawing.
-        auto nvdrv = Nvidia::nvdrv.lock();
-        ASSERT(nvdrv);
-
-        // TODO(Subv): Support more than just disp0. The display device selection is probably based
-        // on which display we're drawing (Default, Internal, External, etc)
-        auto nvdisp = nvdrv->GetDevice<Nvidia::Devices::nvdisp_disp0>("/dev/nvdisp_disp0");
-        ASSERT(nvdisp);
-
-        nvdisp->flip(igbp_buffer.gpu_buffer_id, igbp_buffer.offset, igbp_buffer.format,
-                     igbp_buffer.width, igbp_buffer.height, igbp_buffer.stride);
-
-        buffer_queue->ReleaseBuffer(buffer->slot);
-    }
-}
-
-BufferQueue::BufferQueue(u32 id, u64 layer_id) : id(id), layer_id(layer_id) {
-    native_handle = Kernel::Event::Create(Kernel::ResetType::OneShot, "BufferQueue NativeHandle");
-}
-
-void BufferQueue::SetPreallocatedBuffer(u32 slot, IGBPBuffer& igbp_buffer) {
-    Buffer buffer{};
-    buffer.slot = slot;
-    buffer.igbp_buffer = igbp_buffer;
-    buffer.status = Buffer::Status::Free;
-
-    LOG_WARNING(Service, "Adding graphics buffer %u", slot);
-
-    queue.emplace_back(buffer);
-}
-
-u32 BufferQueue::DequeueBuffer(u32 pixel_format, u32 width, u32 height) {
-    auto itr = std::find_if(queue.begin(), queue.end(), [&](const Buffer& buffer) {
-        // Only consider free buffers. Buffers become free once again after they've been Acquired
-        // and Released by the compositor, see the NVFlinger::Compose method.
-        if (buffer.status != Buffer::Status::Free)
-            return false;
-
-        // Make sure that the parameters match.
-        auto& igbp_buffer = buffer.igbp_buffer;
-        return igbp_buffer.format == pixel_format && igbp_buffer.width == width &&
-               igbp_buffer.height == height;
-    });
-    ASSERT(itr != queue.end());
-
-    itr->status = Buffer::Status::Dequeued;
-    return itr->slot;
-}
-
-const IGBPBuffer& BufferQueue::RequestBuffer(u32 slot) const {
-    auto itr = std::find_if(queue.begin(), queue.end(),
-                            [&](const Buffer& buffer) { return buffer.slot == slot; });
-    ASSERT(itr != queue.end());
-    ASSERT(itr->status == Buffer::Status::Dequeued);
-    return itr->igbp_buffer;
-}
-
-void BufferQueue::QueueBuffer(u32 slot) {
-    auto itr = std::find_if(queue.begin(), queue.end(),
-                            [&](const Buffer& buffer) { return buffer.slot == slot; });
-    ASSERT(itr != queue.end());
-    ASSERT(itr->status == Buffer::Status::Dequeued);
-    itr->status = Buffer::Status::Queued;
-}
-
-boost::optional<const BufferQueue::Buffer&> BufferQueue::AcquireBuffer() {
-    auto itr = std::find_if(queue.begin(), queue.end(), [](const Buffer& buffer) {
-        return buffer.status == Buffer::Status::Queued;
-    });
-    if (itr == queue.end())
-        return boost::none;
-    itr->status = Buffer::Status::Acquired;
-    return *itr;
-}
-
-void BufferQueue::ReleaseBuffer(u32 slot) {
-    auto itr = std::find_if(queue.begin(), queue.end(),
-                            [&](const Buffer& buffer) { return buffer.slot == slot; });
-    ASSERT(itr != queue.end());
-    ASSERT(itr->status == Buffer::Status::Acquired);
-    itr->status = Buffer::Status::Free;
-}
-
-u32 BufferQueue::Query(QueryType type) {
-    LOG_WARNING(Service, "(STUBBED) called type=%u", static_cast<u32>(type));
-    switch (type) {
-    case QueryType::NativeWindowFormat:
-        // TODO(Subv): Use an enum for this
-        static constexpr u32 FormatABGR8 = 1;
-        return FormatABGR8;
-    }
-
-    UNIMPLEMENTED();
-    return 0;
-}
-
-Layer::Layer(u64 id, std::shared_ptr<BufferQueue> queue) : id(id), buffer_queue(std::move(queue)) {}
-
-Display::Display(u64 id, std::string name) : id(id), name(std::move(name)) {
-    vsync_event = Kernel::Event::Create(Kernel::ResetType::Pulse, "Display VSync Event");
-}
-
 } // namespace VI
 } // namespace Service
diff --git a/src/core/hle/service/vi/vi.h b/src/core/hle/service/vi/vi.h
index 4dd4d17833..5e9b7e6cf2 100644
--- a/src/core/hle/service/vi/vi.h
+++ b/src/core/hle/service/vi/vi.h
@@ -7,6 +7,7 @@
 #include <memory>
 #include <boost/optional.hpp>
 #include "core/hle/kernel/event.h"
+#include "core/hle/service/nvflinger/nvflinger.h"
 #include "core/hle/service/service.h"
 
 namespace CoreTiming {
@@ -16,134 +17,9 @@ struct EventType;
 namespace Service {
 namespace VI {
 
-struct IGBPBuffer {
-    u32_le magic;
-    u32_le width;
-    u32_le height;
-    u32_le stride;
-    u32_le format;
-    u32_le usage;
-    INSERT_PADDING_WORDS(1);
-    u32_le index;
-    INSERT_PADDING_WORDS(3);
-    u32_le gpu_buffer_id;
-    INSERT_PADDING_WORDS(17);
-    u32_le nvmap_handle;
-    u32_le offset;
-    INSERT_PADDING_WORDS(60);
-};
-
-static_assert(sizeof(IGBPBuffer) == 0x16C, "IGBPBuffer has wrong size");
-
-class BufferQueue {
-public:
-    enum class QueryType {
-        NativeWindowWidth = 0,
-        NativeWindowHeight = 1,
-        NativeWindowFormat = 2,
-    };
-
-    BufferQueue(u32 id, u64 layer_id);
-    ~BufferQueue() = default;
-
-    struct Buffer {
-        enum class Status { Free = 0, Queued = 1, Dequeued = 2, Acquired = 3 };
-
-        u32 slot;
-        Status status = Status::Free;
-        IGBPBuffer igbp_buffer;
-    };
-
-    void SetPreallocatedBuffer(u32 slot, IGBPBuffer& buffer);
-    u32 DequeueBuffer(u32 pixel_format, u32 width, u32 height);
-    const IGBPBuffer& RequestBuffer(u32 slot) const;
-    void QueueBuffer(u32 slot);
-    boost::optional<const Buffer&> AcquireBuffer();
-    void ReleaseBuffer(u32 slot);
-    u32 Query(QueryType type);
-
-    u32 GetId() const {
-        return id;
-    }
-
-    Kernel::SharedPtr<Kernel::Event> GetNativeHandle() const {
-        return native_handle;
-    }
-
-private:
-    u32 id;
-    u64 layer_id;
-
-    std::vector<Buffer> queue;
-    Kernel::SharedPtr<Kernel::Event> native_handle;
-};
-
-struct Layer {
-    Layer(u64 id, std::shared_ptr<BufferQueue> queue);
-    ~Layer() = default;
-
-    u64 id;
-    std::shared_ptr<BufferQueue> buffer_queue;
-};
-
-struct Display {
-    Display(u64 id, std::string name);
-    ~Display() = default;
-
-    u64 id;
-    std::string name;
-
-    std::vector<Layer> layers;
-    Kernel::SharedPtr<Kernel::Event> vsync_event;
-};
-
-class NVFlinger {
-public:
-    NVFlinger();
-    ~NVFlinger();
-
-    /// Opens the specified display and returns the id.
-    u64 OpenDisplay(const std::string& name);
-
-    /// Creates a layer on the specified display and returns the layer id.
-    u64 CreateLayer(u64 display_id);
-
-    /// Gets the buffer queue id of the specified layer in the specified display.
-    u32 GetBufferQueueId(u64 display_id, u64 layer_id);
-
-    /// Gets the vsync event for the specified display.
-    Kernel::SharedPtr<Kernel::Event> GetVsyncEvent(u64 display_id);
-
-    /// Obtains a buffer queue identified by the id.
-    std::shared_ptr<BufferQueue> GetBufferQueue(u32 id) const;
-
-    /// Performs a composition request to the emulated nvidia GPU and triggers the vsync events when
-    /// finished.
-    void Compose();
-
-private:
-    /// Returns the display identified by the specified id.
-    Display& GetDisplay(u64 display_id);
-
-    /// Returns the layer identified by the specified id in the desired display.
-    Layer& GetLayer(u64 display_id, u64 layer_id);
-
-    std::vector<Display> displays;
-    std::vector<std::shared_ptr<BufferQueue>> buffer_queues;
-
-    /// Id to use for the next layer that is created, this counter is shared among all displays.
-    u64 next_layer_id = 1;
-    /// Id to use for the next buffer queue that is created, this counter is shared among all
-    /// layers.
-    u32 next_buffer_queue_id = 1;
-
-    /// CoreTiming event that handles screen composition.
-    CoreTiming::EventType* composition_event;
-};
-
 class IApplicationDisplayService final : public ServiceFramework<IApplicationDisplayService> {
 public:
-    IApplicationDisplayService(std::shared_ptr<NVFlinger> nv_flinger);
+    IApplicationDisplayService(std::shared_ptr<NVFlinger::NVFlinger> nv_flinger);
     ~IApplicationDisplayService() = default;
 
 private:
@@ -159,7 +35,7 @@ private:
     void DestroyStrayLayer(Kernel::HLERequestContext& ctx);
     void GetDisplayVsyncEvent(Kernel::HLERequestContext& ctx);
 
-    std::shared_ptr<NVFlinger> nv_flinger;
+    std::shared_ptr<NVFlinger::NVFlinger> nv_flinger;
 };
 
 /// Registers all VI services with the specified service manager.
diff --git a/src/core/hle/service/vi/vi_m.cpp b/src/core/hle/service/vi/vi_m.cpp
index 1a5a28b0d8..6deedf8423 100644
--- a/src/core/hle/service/vi/vi_m.cpp
+++ b/src/core/hle/service/vi/vi_m.cpp
@@ -23,7 +23,7 @@ VI_M::VI_M() : ServiceFramework("vi:m") {
         {3, nullptr, "GetDisplayServiceWithProxyNameExchange"},
     };
     RegisterHandlers(functions);
-    nv_flinger = std::make_shared<NVFlinger>();
+    nv_flinger = std::make_shared<NVFlinger::NVFlinger>();
 }
 
 } // namespace VI
diff --git a/src/core/hle/service/vi/vi_m.h b/src/core/hle/service/vi/vi_m.h
index 70ff7a2f38..ebe79d5c73 100644
--- a/src/core/hle/service/vi/vi_m.h
+++ b/src/core/hle/service/vi/vi_m.h
@@ -8,6 +8,10 @@
 #include "core/hle/service/service.h"
 
 namespace Service {
+namespace NVFlinger {
+class NVFlinger;
+}
+
 namespace VI {
 
 class VI_M final : public ServiceFramework<VI_M> {
@@ -18,7 +22,7 @@ public:
 private:
     void GetDisplayService(Kernel::HLERequestContext& ctx);
 
-    std::shared_ptr<NVFlinger> nv_flinger;
+    std::shared_ptr<NVFlinger::NVFlinger> nv_flinger;
 };
 
 } // namespace VI