diff --git a/src/core/hle/service/nvflinger/buffer_queue_producer.cpp b/src/core/hle/service/nvflinger/buffer_queue_producer.cpp
index e601b5da1..abed92d06 100644
--- a/src/core/hle/service/nvflinger/buffer_queue_producer.cpp
+++ b/src/core/hle/service/nvflinger/buffer_queue_producer.cpp
@@ -815,8 +815,8 @@ Status BufferQueueProducer::SetPreallocatedBuffer(s32 slot,
 
 void BufferQueueProducer::Transact(Kernel::HLERequestContext& ctx, TransactionId code, u32 flags) {
     Status status{Status::NoError};
-    Parcel parcel_in{ctx.ReadBuffer()};
-    Parcel parcel_out{};
+    InputParcel parcel_in{ctx.ReadBufferSpan()};
+    OutputParcel parcel_out{};
 
     switch (code) {
     case TransactionId::Connect: {
diff --git a/src/core/hle/service/nvflinger/graphic_buffer_producer.cpp b/src/core/hle/service/nvflinger/graphic_buffer_producer.cpp
index 4043c91f1..769e8c0a3 100644
--- a/src/core/hle/service/nvflinger/graphic_buffer_producer.cpp
+++ b/src/core/hle/service/nvflinger/graphic_buffer_producer.cpp
@@ -9,7 +9,7 @@
 
 namespace Service::android {
 
-QueueBufferInput::QueueBufferInput(Parcel& parcel) {
+QueueBufferInput::QueueBufferInput(InputParcel& parcel) {
     parcel.ReadFlattened(*this);
 }
 
diff --git a/src/core/hle/service/nvflinger/graphic_buffer_producer.h b/src/core/hle/service/nvflinger/graphic_buffer_producer.h
index 6ea327bbe..2969f0fd5 100644
--- a/src/core/hle/service/nvflinger/graphic_buffer_producer.h
+++ b/src/core/hle/service/nvflinger/graphic_buffer_producer.h
@@ -14,11 +14,11 @@
 
 namespace Service::android {
 
-class Parcel;
+class InputParcel;
 
 #pragma pack(push, 1)
 struct QueueBufferInput final {
-    explicit QueueBufferInput(Parcel& parcel);
+    explicit QueueBufferInput(InputParcel& parcel);
 
     void Deflate(s64* timestamp_, bool* is_auto_timestamp_, Common::Rectangle<s32>* crop_,
                  NativeWindowScalingMode* scaling_mode_, NativeWindowTransform* transform_,
diff --git a/src/core/hle/service/nvflinger/parcel.h b/src/core/hle/service/nvflinger/parcel.h
index f3fa2587d..d1b6201e0 100644
--- a/src/core/hle/service/nvflinger/parcel.h
+++ b/src/core/hle/service/nvflinger/parcel.h
@@ -4,6 +4,7 @@
 #pragma once
 
 #include <memory>
+#include <span>
 #include <vector>
 
 #include "common/alignment.h"
@@ -12,18 +13,17 @@
 
 namespace Service::android {
 
-class Parcel final {
+struct ParcelHeader {
+    u32 data_size;
+    u32 data_offset;
+    u32 objects_size;
+    u32 objects_offset;
+};
+static_assert(sizeof(ParcelHeader) == 16, "ParcelHeader has wrong size");
+
+class InputParcel final {
 public:
-    static constexpr std::size_t DefaultBufferSize = 0x40;
-
-    Parcel() : buffer(DefaultBufferSize) {}
-
-    template <typename T>
-    explicit Parcel(const T& out_data) : buffer(DefaultBufferSize) {
-        Write(out_data);
-    }
-
-    explicit Parcel(std::vector<u8> in_data) : buffer(std::move(in_data)) {
+    explicit InputParcel(std::span<const u8> in_data) : read_buffer(std::move(in_data)) {
         DeserializeHeader();
         [[maybe_unused]] const std::u16string token = ReadInterfaceToken();
     }
@@ -31,9 +31,9 @@ public:
     template <typename T>
     void Read(T& val) {
         static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable.");
-        ASSERT(read_index + sizeof(T) <= buffer.size());
+        ASSERT(read_index + sizeof(T) <= read_buffer.size());
 
-        std::memcpy(&val, buffer.data() + read_index, sizeof(T));
+        std::memcpy(&val, read_buffer.data() + read_index, sizeof(T));
         read_index += sizeof(T);
         read_index = Common::AlignUp(read_index, 4);
     }
@@ -62,10 +62,10 @@ public:
     template <typename T>
     T ReadUnaligned() {
         static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable.");
-        ASSERT(read_index + sizeof(T) <= buffer.size());
+        ASSERT(read_index + sizeof(T) <= read_buffer.size());
 
         T val;
-        std::memcpy(&val, buffer.data() + read_index, sizeof(T));
+        std::memcpy(&val, read_buffer.data() + read_index, sizeof(T));
         read_index += sizeof(T);
         return val;
     }
@@ -101,6 +101,31 @@ public:
         return token;
     }
 
+    void DeserializeHeader() {
+        ASSERT(read_buffer.size() > sizeof(ParcelHeader));
+
+        ParcelHeader header{};
+        std::memcpy(&header, read_buffer.data(), sizeof(ParcelHeader));
+
+        read_index = header.data_offset;
+    }
+
+private:
+    std::span<const u8> read_buffer;
+    std::size_t read_index = 0;
+};
+
+class OutputParcel final {
+public:
+    static constexpr std::size_t DefaultBufferSize = 0x40;
+
+    OutputParcel() : buffer(DefaultBufferSize) {}
+
+    template <typename T>
+    explicit OutputParcel(const T& out_data) : buffer(DefaultBufferSize) {
+        Write(out_data);
+    }
+
     template <typename T>
     void Write(const T& val) {
         static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable.");
@@ -133,40 +158,20 @@ public:
         WriteObject(ptr.get());
     }
 
-    void DeserializeHeader() {
-        ASSERT(buffer.size() > sizeof(Header));
-
-        Header header{};
-        std::memcpy(&header, buffer.data(), sizeof(Header));
-
-        read_index = header.data_offset;
-    }
-
     std::vector<u8> Serialize() const {
-        ASSERT(read_index == 0);
-
-        Header header{};
-        header.data_size = static_cast<u32>(write_index - sizeof(Header));
-        header.data_offset = sizeof(Header);
+        ParcelHeader header{};
+        header.data_size = static_cast<u32>(write_index - sizeof(ParcelHeader));
+        header.data_offset = sizeof(ParcelHeader);
         header.objects_size = 4;
-        header.objects_offset = static_cast<u32>(sizeof(Header) + header.data_size);
-        std::memcpy(buffer.data(), &header, sizeof(Header));
+        header.objects_offset = static_cast<u32>(sizeof(ParcelHeader) + header.data_size);
+        std::memcpy(buffer.data(), &header, sizeof(ParcelHeader));
 
         return buffer;
     }
 
 private:
-    struct Header {
-        u32 data_size;
-        u32 data_offset;
-        u32 objects_size;
-        u32 objects_offset;
-    };
-    static_assert(sizeof(Header) == 16, "ParcelHeader has wrong size");
-
     mutable std::vector<u8> buffer;
-    std::size_t read_index = 0;
-    std::size_t write_index = sizeof(Header);
+    std::size_t write_index = sizeof(ParcelHeader);
 };
 
 } // namespace Service::android
diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp
index bb283e74e..2fb631183 100644
--- a/src/core/hle/service/vi/vi.cpp
+++ b/src/core/hle/service/vi/vi.cpp
@@ -603,7 +603,7 @@ private:
             return;
         }
 
-        const auto parcel = android::Parcel{NativeWindow{*buffer_queue_id}};
+        const auto parcel = android::OutputParcel{NativeWindow{*buffer_queue_id}};
         const auto buffer_size = ctx.WriteBuffer(parcel.Serialize());
 
         IPC::ResponseBuilder rb{ctx, 4};
@@ -649,7 +649,7 @@ private:
             return;
         }
 
-        const auto parcel = android::Parcel{NativeWindow{*buffer_queue_id}};
+        const auto parcel = android::OutputParcel{NativeWindow{*buffer_queue_id}};
         const auto buffer_size = ctx.WriteBuffer(parcel.Serialize());
 
         IPC::ResponseBuilder rb{ctx, 6};