Memory manager: buffer uploads (D3D) - Part 1

Manages a single persistently mapped GPU heap which is sub-allocated
inside of ring-buffer for uploads. To handle larger buffers without additional
unused heaps, ring buffers are created on-demand.

BUG=dawn:28
TEST=dawn_unittests

Change-Id: Ifc5a1b06baf8633f1e133245ac1ee76275431cc5
Reviewed-on: https://dawn-review.googlesource.com/c/3160
Commit-Queue: Bryan Bernhart <bryan.bernhart@intel.com>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index 99d1ae4..19bc998 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -451,6 +451,8 @@
     "src/dawn_native/ComputePipeline.h",
     "src/dawn_native/Device.cpp",
     "src/dawn_native/Device.h",
+    "src/dawn_native/DynamicUploader.cpp",
+    "src/dawn_native/DynamicUploader.h",
     "src/dawn_native/Error.cpp",
     "src/dawn_native/Error.h",
     "src/dawn_native/ErrorData.cpp",
@@ -485,10 +487,14 @@
     "src/dawn_native/RenderPassEncoder.h",
     "src/dawn_native/RenderPipeline.cpp",
     "src/dawn_native/RenderPipeline.h",
+    "src/dawn_native/RingBuffer.cpp",
+    "src/dawn_native/RingBuffer.h",
     "src/dawn_native/Sampler.cpp",
     "src/dawn_native/Sampler.h",
     "src/dawn_native/ShaderModule.cpp",
     "src/dawn_native/ShaderModule.h",
+    "src/dawn_native/StagingBuffer.cpp",
+    "src/dawn_native/StagingBuffer.h",
     "src/dawn_native/SwapChain.cpp",
     "src/dawn_native/SwapChain.h",
     "src/dawn_native/Texture.cpp",
@@ -533,12 +539,12 @@
       "src/dawn_native/d3d12/RenderPipelineD3D12.h",
       "src/dawn_native/d3d12/ResourceAllocator.cpp",
       "src/dawn_native/d3d12/ResourceAllocator.h",
-      "src/dawn_native/d3d12/ResourceUploader.cpp",
-      "src/dawn_native/d3d12/ResourceUploader.h",
       "src/dawn_native/d3d12/SamplerD3D12.cpp",
       "src/dawn_native/d3d12/SamplerD3D12.h",
       "src/dawn_native/d3d12/ShaderModuleD3D12.cpp",
       "src/dawn_native/d3d12/ShaderModuleD3D12.h",
+      "src/dawn_native/d3d12/StagingBufferD3D12.cpp",
+      "src/dawn_native/d3d12/StagingBufferD3D12.h",
       "src/dawn_native/d3d12/SwapChainD3D12.cpp",
       "src/dawn_native/d3d12/SwapChainD3D12.h",
       "src/dawn_native/d3d12/TextureCopySplitter.cpp",
@@ -899,6 +905,7 @@
     "src/tests/unittests/PerStageTests.cpp",
     "src/tests/unittests/RefCountedTests.cpp",
     "src/tests/unittests/ResultTests.cpp",
+    "src/tests/unittests/RingBufferTests.cpp",
     "src/tests/unittests/SerialMapTests.cpp",
     "src/tests/unittests/SerialQueueTests.cpp",
     "src/tests/unittests/ToBackendTests.cpp",
diff --git a/src/common/SerialStorage.h b/src/common/SerialStorage.h
index 60fae47..6f38213 100644
--- a/src/common/SerialStorage.h
+++ b/src/common/SerialStorage.h
@@ -110,6 +110,7 @@
     void ClearUpTo(Serial serial);
 
     Serial FirstSerial() const;
+    Serial LastSerial() const;
 
   protected:
     // Returns the first StorageIterator that a serial bigger than serial.
@@ -163,6 +164,12 @@
 }
 
 template <typename Derived>
+Serial SerialStorage<Derived>::LastSerial() const {
+    DAWN_ASSERT(!Empty());
+    return mStorage.back().first;
+}
+
+template <typename Derived>
 typename SerialStorage<Derived>::ConstStorageIterator SerialStorage<Derived>::FindUpTo(
     Serial serial) const {
     auto it = mStorage.begin();
diff --git a/src/dawn_native/Buffer.cpp b/src/dawn_native/Buffer.cpp
index 8873567..543d62e 100644
--- a/src/dawn_native/Buffer.cpp
+++ b/src/dawn_native/Buffer.cpp
@@ -106,7 +106,9 @@
             return;
         }
 
-        SetSubDataImpl(start, count, data);
+        if (GetDevice()->ConsumedError(SetSubDataImpl(start, count, data))) {
+            return;
+        }
     }
 
     void BufferBase::MapReadAsync(uint32_t start,
diff --git a/src/dawn_native/Buffer.h b/src/dawn_native/Buffer.h
index 9a3fa2c..4472cec 100644
--- a/src/dawn_native/Buffer.h
+++ b/src/dawn_native/Buffer.h
@@ -63,7 +63,7 @@
         void CallMapWriteCallback(uint32_t serial, dawnBufferMapAsyncStatus status, void* pointer);
 
       private:
-        virtual void SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) = 0;
+        virtual MaybeError SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) = 0;
         virtual void MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t size) = 0;
         virtual void MapWriteAsyncImpl(uint32_t serial, uint32_t start, uint32_t size) = 0;
         virtual void UnmapImpl() = 0;
diff --git a/src/dawn_native/Device.h b/src/dawn_native/Device.h
index 82e2cf8..fc79075 100644
--- a/src/dawn_native/Device.h
+++ b/src/dawn_native/Device.h
@@ -31,6 +31,8 @@
 
     class AdapterBase;
     class FenceSignalTracker;
+    class DynamicUploader;
+    class StagingBufferBase;
 
     class DeviceBase {
       public:
@@ -60,6 +62,7 @@
 
         virtual Serial GetCompletedCommandSerial() const = 0;
         virtual Serial GetLastSubmittedCommandSerial() const = 0;
+        virtual Serial GetPendingCommandSerial() const = 0;
         virtual void TickImpl() = 0;
 
         // Many Dawn objects are completely immutable once created which means that if two
@@ -111,6 +114,14 @@
 
         virtual const PCIInfo& GetPCIInfo() const;
 
+        virtual ResultOrError<std::unique_ptr<StagingBufferBase>> CreateStagingBuffer(
+            size_t size) = 0;
+        virtual MaybeError CopyFromStagingToBuffer(StagingBufferBase* source,
+                                                   uint32_t sourceOffset,
+                                                   BufferBase* destination,
+                                                   uint32_t destinationOffset,
+                                                   uint32_t size) = 0;
+
       private:
         virtual ResultOrError<BindGroupBase*> CreateBindGroupImpl(
             const BindGroupDescriptor* descriptor) = 0;
diff --git a/src/dawn_native/DynamicUploader.cpp b/src/dawn_native/DynamicUploader.cpp
new file mode 100644
index 0000000..e127db4
--- /dev/null
+++ b/src/dawn_native/DynamicUploader.cpp
@@ -0,0 +1,82 @@
+// Copyright 2018 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "dawn_native/DynamicUploader.h"
+#include "common/Math.h"
+#include "dawn_native/Device.h"
+
+namespace dawn_native {
+
+    DynamicUploader::DynamicUploader(DeviceBase* device) : mDevice(device) {
+    }
+
+    MaybeError DynamicUploader::CreateAndAppendBuffer(size_t size) {
+        std::unique_ptr<RingBuffer> ringBuffer = std::make_unique<RingBuffer>(mDevice, size);
+        DAWN_TRY(ringBuffer->Initialize());
+        mRingBuffers.emplace_back(std::move(ringBuffer));
+        return {};
+    }
+
+    ResultOrError<UploadHandle> DynamicUploader::Allocate(uint32_t size, uint32_t alignment) {
+        ASSERT(IsPowerOfTwo(alignment));
+
+        // Align the requested allocation size
+        const size_t alignedSize = Align(size, alignment);
+
+        RingBuffer* largestRingBuffer = GetLargestBuffer();
+        UploadHandle uploadHandle = largestRingBuffer->SubAllocate(alignedSize);
+
+        // Upon failure, append a newly created (and much larger) ring buffer to fulfill the
+        // request.
+        if (uploadHandle.mappedBuffer == nullptr) {
+            // Compute the new max size (in powers of two to preserve alignment).
+            size_t newMaxSize = largestRingBuffer->GetSize() * 2;
+            while (newMaxSize < size) {
+                newMaxSize *= 2;
+            }
+
+            // TODO(b-brber): Fall-back to no sub-allocations should this fail.
+            DAWN_TRY(CreateAndAppendBuffer(newMaxSize));
+            largestRingBuffer = GetLargestBuffer();
+            uploadHandle = largestRingBuffer->SubAllocate(alignedSize);
+        }
+
+        uploadHandle.stagingBuffer = largestRingBuffer->GetStagingBuffer();
+
+        return uploadHandle;
+    }
+
+    void DynamicUploader::Tick(Serial lastCompletedSerial) {
+        // Reclaim memory within the ring buffers by ticking (or removing requests no longer
+        // in-flight).
+        for (size_t i = 0; i < mRingBuffers.size(); ++i) {
+            mRingBuffers[i]->Tick(lastCompletedSerial);
+
+            // Never erase the last buffer as to prevent re-creating smaller buffers
+            // again. The last buffer is the largest.
+            if (mRingBuffers[i]->Empty() && i < mRingBuffers.size() - 1) {
+                mRingBuffers.erase(mRingBuffers.begin() + i);
+            }
+        }
+    }
+
+    RingBuffer* DynamicUploader::GetLargestBuffer() {
+        ASSERT(!mRingBuffers.empty());
+        return mRingBuffers.back().get();
+    }
+
+    bool DynamicUploader::IsEmpty() const {
+        return mRingBuffers.empty();
+    }
+}  // namespace dawn_native
\ No newline at end of file
diff --git a/src/dawn_native/DynamicUploader.h b/src/dawn_native/DynamicUploader.h
new file mode 100644
index 0000000..848c07a
--- /dev/null
+++ b/src/dawn_native/DynamicUploader.h
@@ -0,0 +1,47 @@
+// Copyright 2018 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef DAWNNATIVE_DYNAMICUPLOADER_H_
+#define DAWNNATIVE_DYNAMICUPLOADER_H_
+
+#include "dawn_native/Forward.h"
+#include "dawn_native/RingBuffer.h"
+
+#include <memory>
+
+// DynamicUploader is the front-end implementation used to manage multiple ring buffers for upload
+// usage.
+namespace dawn_native {
+
+    class DynamicUploader {
+      public:
+        DynamicUploader(DeviceBase* device);
+        ~DynamicUploader() = default;
+
+        ResultOrError<UploadHandle> Allocate(uint32_t requiredSize, uint32_t alignment);
+        void Tick(Serial lastCompletedSerial);
+
+        RingBuffer* GetLargestBuffer();
+
+        MaybeError CreateAndAppendBuffer(size_t size);
+
+        bool IsEmpty() const;
+
+      private:
+        std::vector<std::unique_ptr<RingBuffer>> mRingBuffers;
+        DeviceBase* mDevice;
+    };
+}  // namespace dawn_native
+
+#endif  // DAWNNATIVE_DYNAMICUPLOADER_H_
\ No newline at end of file
diff --git a/src/dawn_native/Forward.h b/src/dawn_native/Forward.h
index 5373f66..73a2555 100644
--- a/src/dawn_native/Forward.h
+++ b/src/dawn_native/Forward.h
@@ -44,6 +44,7 @@
     class SamplerBase;
     class ShaderModuleBase;
     class ShaderModuleBuilder;
+    class StagingBufferBase;
     class SwapChainBase;
     class SwapChainBuilder;
     class TextureBase;
diff --git a/src/dawn_native/RingBuffer.cpp b/src/dawn_native/RingBuffer.cpp
new file mode 100644
index 0000000..51e97c2
--- /dev/null
+++ b/src/dawn_native/RingBuffer.cpp
@@ -0,0 +1,148 @@
+// Copyright 2018 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "dawn_native/RingBuffer.h"
+#include "dawn_native/Device.h"
+
+// Note: Current RingBuffer implementation uses two indices (start and end) to implement a circular
+// queue. However, this approach defines a full queue when one element is still unused.
+//
+// For example, [E,E,E,E] would be equivelent to [U,U,U,U].
+//                 ^                                ^
+//                S=E=1                            S=E=1
+//
+// The latter case is eliminated by counting used bytes >= capacity. This definition prevents
+// (the last) byte and requires an extra variable to count used bytes. Alternatively, we could use
+// only two indices that keep increasing (unbounded) but can be still indexed using bit masks.
+// However, this 1) requires the size to always be a power-of-two and 2) remove tests that check
+// used bytes.
+// TODO(b-brber): Follow-up with ringbuffer optimization.
+namespace dawn_native {
+
+    static constexpr size_t INVALID_OFFSET = std::numeric_limits<size_t>::max();
+
+    RingBuffer::RingBuffer(DeviceBase* device, size_t size) : mBufferSize(size), mDevice(device) {
+    }
+
+    MaybeError RingBuffer::Initialize() {
+        DAWN_TRY_ASSIGN(mStagingBuffer, mDevice->CreateStagingBuffer(mBufferSize));
+        DAWN_TRY(mStagingBuffer->Initialize());
+        return {};
+    }
+
+    // Record allocations in a request when serial advances.
+    // This method has been split from Tick() for testing.
+    void RingBuffer::Track() {
+        if (mCurrentRequestSize == 0)
+            return;
+        const Serial currentSerial = mDevice->GetPendingCommandSerial();
+        if (mInflightRequests.Empty() || currentSerial > mInflightRequests.LastSerial()) {
+            Request request;
+            request.endOffset = mUsedEndOffset;
+            request.size = mCurrentRequestSize;
+
+            mInflightRequests.Enqueue(std::move(request), currentSerial);
+            mCurrentRequestSize = 0;  // reset
+        }
+    }
+
+    void RingBuffer::Tick(Serial lastCompletedSerial) {
+        Track();
+
+        // Reclaim memory from previously recorded blocks.
+        for (Request& request : mInflightRequests.IterateUpTo(lastCompletedSerial)) {
+            mUsedStartOffset = request.endOffset;
+            mUsedSize -= request.size;
+        }
+
+        // Dequeue previously recorded requests.
+        mInflightRequests.ClearUpTo(lastCompletedSerial);
+    }
+
+    size_t RingBuffer::GetSize() const {
+        return mBufferSize;
+    }
+
+    size_t RingBuffer::GetUsedSize() const {
+        return mUsedSize;
+    }
+
+    bool RingBuffer::Empty() const {
+        return mInflightRequests.Empty();
+    }
+
+    StagingBufferBase* RingBuffer::GetStagingBuffer() const {
+        ASSERT(mStagingBuffer != nullptr);
+        return mStagingBuffer.get();
+    }
+
+    // Sub-allocate the ring-buffer by requesting a chunk of the specified size.
+    // This is a serial-based resource scheme, the life-span of resources (and the allocations) get
+    // tracked by GPU progress via serials. Memory can be reused by determining if the GPU has
+    // completed up to a given serial. Each sub-allocation request is tracked in the serial offset
+    // queue, which identifies an existing (or new) frames-worth of resources. Internally, the
+    // ring-buffer maintains offsets of 3 "memory" states: Free, Reclaimed, and Used. This is done
+    // in FIFO order as older frames would free resources before newer ones.
+    UploadHandle RingBuffer::SubAllocate(size_t allocSize) {
+        ASSERT(mStagingBuffer != nullptr);
+
+        // Check if the buffer is full by comparing the used size.
+        // If the buffer is not split where waste occurs (e.g. cannot fit new sub-alloc in front), a
+        // subsequent sub-alloc could fail where the used size was previously adjusted to include
+        // the wasted.
+        if (mUsedSize >= mBufferSize)
+            return UploadHandle{};
+
+        size_t startOffset = INVALID_OFFSET;
+
+        // Check if the buffer is NOT split (i.e sub-alloc on ends)
+        if (mUsedStartOffset <= mUsedEndOffset) {
+            // Order is important (try to sub-alloc at end first).
+            // This is due to FIFO order where sub-allocs are inserted from left-to-right (when not
+            // wrapped).
+            if (mUsedEndOffset + allocSize <= mBufferSize) {
+                startOffset = mUsedEndOffset;
+                mUsedEndOffset += allocSize;
+                mUsedSize += allocSize;
+                mCurrentRequestSize += allocSize;
+            } else if (allocSize <= mUsedStartOffset) {  // Try to sub-alloc at front.
+                // Count the space at front in the request size so that a subsequent
+                // sub-alloc cannot not succeed when the buffer is full.
+                const size_t requestSize = (mBufferSize - mUsedEndOffset) + allocSize;
+
+                startOffset = 0;
+                mUsedEndOffset = allocSize;
+                mUsedSize += requestSize;
+                mCurrentRequestSize += requestSize;
+            }
+        } else if (mUsedEndOffset + allocSize <=
+                   mUsedStartOffset) {  // Otherwise, buffer is split where sub-alloc must be
+                                        // in-between.
+            startOffset = mUsedEndOffset;
+            mUsedEndOffset += allocSize;
+            mUsedSize += allocSize;
+            mCurrentRequestSize += allocSize;
+        }
+
+        if (startOffset == INVALID_OFFSET)
+            return UploadHandle{};
+
+        UploadHandle uploadHandle;
+        uploadHandle.mappedBuffer =
+            static_cast<uint8_t*>(mStagingBuffer->GetMappedPointer()) + startOffset;
+        uploadHandle.startOffset = startOffset;
+
+        return uploadHandle;
+    }
+}  // namespace dawn_native
\ No newline at end of file
diff --git a/src/dawn_native/RingBuffer.h b/src/dawn_native/RingBuffer.h
new file mode 100644
index 0000000..dfa44f5
--- /dev/null
+++ b/src/dawn_native/RingBuffer.h
@@ -0,0 +1,72 @@
+// Copyright 2018 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef DAWNNATIVE_RINGBUFFER_H_
+#define DAWNNATIVE_RINGBUFFER_H_
+
+#include "common/SerialQueue.h"
+#include "dawn_native/StagingBuffer.h"
+
+// RingBuffer is the front-end implementation used to manage a ring buffer in GPU memory.
+namespace dawn_native {
+
+    struct UploadHandle {
+        uint8_t* mappedBuffer = nullptr;
+        size_t startOffset = 0;
+        StagingBufferBase* stagingBuffer = nullptr;
+    };
+
+    class DeviceBase;
+
+    class RingBuffer {
+      public:
+        RingBuffer(DeviceBase* device, size_t size);
+        ~RingBuffer() = default;
+
+        MaybeError Initialize();
+
+        UploadHandle SubAllocate(size_t requestedSize);
+
+        void Tick(Serial lastCompletedSerial);
+        size_t GetSize() const;
+        bool Empty() const;
+        size_t GetUsedSize() const;
+        StagingBufferBase* GetStagingBuffer() const;
+
+        // Seperated for testing.
+        void Track();
+
+      private:
+        std::unique_ptr<StagingBufferBase> mStagingBuffer;
+
+        struct Request {
+            size_t endOffset;
+            size_t size;
+        };
+
+        SerialQueue<Request> mInflightRequests;  // Queue of the recorded sub-alloc requests (e.g.
+                                                 // frame of resources).
+
+        size_t mUsedEndOffset = 0;    // Tail of used sub-alloc requests (in bytes).
+        size_t mUsedStartOffset = 0;  // Head of used sub-alloc requests (in bytes).
+        size_t mBufferSize = 0;       // Max size of the ring buffer (in bytes).
+        size_t mUsedSize = 0;  // Size of the sub-alloc requests (in bytes) of the ring buffer.
+        size_t mCurrentRequestSize =
+            0;  // Size of the sub-alloc requests (in bytes) of the current serial.
+
+        DeviceBase* mDevice;
+    };
+}  // namespace dawn_native
+
+#endif  // DAWNNATIVE_RINGBUFFER_H_
\ No newline at end of file
diff --git a/src/dawn_native/StagingBuffer.cpp b/src/dawn_native/StagingBuffer.cpp
new file mode 100644
index 0000000..51f5fa8
--- /dev/null
+++ b/src/dawn_native/StagingBuffer.cpp
@@ -0,0 +1,29 @@
+// Copyright 2018 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "dawn_native/StagingBuffer.h"
+
+namespace dawn_native {
+
+    StagingBufferBase::StagingBufferBase(size_t size) : mBufferSize(size) {
+    }
+
+    size_t StagingBufferBase::GetSize() const {
+        return mBufferSize;
+    }
+
+    void* StagingBufferBase::GetMappedPointer() const {
+        return mMappedPointer;
+    }
+}  // namespace dawn_native
\ No newline at end of file
diff --git a/src/dawn_native/StagingBuffer.h b/src/dawn_native/StagingBuffer.h
new file mode 100644
index 0000000..1da8900
--- /dev/null
+++ b/src/dawn_native/StagingBuffer.h
@@ -0,0 +1,41 @@
+// Copyright 2018 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef DAWNNATIVE_STAGINGBUFFER_H_
+#define DAWNNATIVE_STAGINGBUFFER_H_
+
+#include "dawn_native/Error.h"
+
+namespace dawn_native {
+
+    class StagingBufferBase {
+      public:
+        StagingBufferBase(size_t size);
+        virtual ~StagingBufferBase() = default;
+
+        virtual MaybeError Initialize() = 0;
+
+        void* GetMappedPointer() const;
+        size_t GetSize() const;
+
+      protected:
+        void* mMappedPointer = nullptr;
+
+      private:
+        const size_t mBufferSize;
+    };
+
+}  // namespace dawn_native
+
+#endif  // DAWNNATIVE_STAGINGBUFFER_H_
\ No newline at end of file
diff --git a/src/dawn_native/ToBackend.h b/src/dawn_native/ToBackend.h
index 585a40d..1ad86a8 100644
--- a/src/dawn_native/ToBackend.h
+++ b/src/dawn_native/ToBackend.h
@@ -94,6 +94,11 @@
     };
 
     template <typename BackendTraits>
+    struct ToBackendTraits<StagingBufferBase, BackendTraits> {
+        using BackendType = typename BackendTraits::StagingBufferType;
+    };
+
+    template <typename BackendTraits>
     struct ToBackendTraits<TextureBase, BackendTraits> {
         using BackendType = typename BackendTraits::TextureType;
     };
diff --git a/src/dawn_native/d3d12/BufferD3D12.cpp b/src/dawn_native/d3d12/BufferD3D12.cpp
index 4b75d1f..69c25aa 100644
--- a/src/dawn_native/d3d12/BufferD3D12.cpp
+++ b/src/dawn_native/d3d12/BufferD3D12.cpp
@@ -17,9 +17,9 @@
 #include "common/Assert.h"
 #include "common/Constants.h"
 #include "common/Math.h"
+#include "dawn_native/DynamicUploader.h"
 #include "dawn_native/d3d12/DeviceD3D12.h"
 #include "dawn_native/d3d12/ResourceAllocator.h"
-#include "dawn_native/d3d12/ResourceUploader.h"
 
 namespace dawn_native { namespace d3d12 {
 
@@ -161,11 +161,22 @@
         }
     }
 
-    void Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
+    MaybeError Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
         Device* device = ToBackend(GetDevice());
 
-        TransitionUsageNow(device->GetPendingCommandList(), dawn::BufferUsageBit::TransferDst);
-        device->GetResourceUploader()->BufferSubData(mResource, start, count, data);
+        DynamicUploader* uploader = nullptr;
+        DAWN_TRY_ASSIGN(uploader, device->GetDynamicUploader());
+
+        UploadHandle uploadHandle;
+        DAWN_TRY_ASSIGN(uploadHandle, uploader->Allocate(count, kDefaultAlignment));
+        ASSERT(uploadHandle.mappedBuffer != nullptr);
+
+        memcpy(uploadHandle.mappedBuffer, data, count);
+
+        DAWN_TRY(device->CopyFromStagingToBuffer(uploadHandle.stagingBuffer,
+                                                 uploadHandle.startOffset, this, start, count));
+
+        return {};
     }
 
     void Buffer::MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) {
diff --git a/src/dawn_native/d3d12/BufferD3D12.h b/src/dawn_native/d3d12/BufferD3D12.h
index 633d260..753aac4 100644
--- a/src/dawn_native/d3d12/BufferD3D12.h
+++ b/src/dawn_native/d3d12/BufferD3D12.h
@@ -39,11 +39,15 @@
 
       private:
         // Dawn API
-        void SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
+        MaybeError SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
         void MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void MapWriteAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void UnmapImpl() override;
 
+        // TODO(b-brber): Remove once alignment constraint is added to validation (dawn:73).
+        static constexpr size_t kDefaultAlignment =
+            4;  // D3D does not specify so we assume 4-byte alignment to be safe.
+
         ComPtr<ID3D12Resource> mResource;
         bool mFixedResourceState = false;
         dawn::BufferUsageBit mLastUsage = dawn::BufferUsageBit::None;
diff --git a/src/dawn_native/d3d12/DeviceD3D12.cpp b/src/dawn_native/d3d12/DeviceD3D12.cpp
index a72ec89..0ad3f4d 100644
--- a/src/dawn_native/d3d12/DeviceD3D12.cpp
+++ b/src/dawn_native/d3d12/DeviceD3D12.cpp
@@ -16,6 +16,7 @@
 
 #include "common/Assert.h"
 #include "dawn_native/BackendConnection.h"
+#include "dawn_native/DynamicUploader.h"
 #include "dawn_native/d3d12/BindGroupD3D12.h"
 #include "dawn_native/d3d12/BindGroupLayoutD3D12.h"
 #include "dawn_native/d3d12/BufferD3D12.h"
@@ -30,9 +31,9 @@
 #include "dawn_native/d3d12/RenderPassDescriptorD3D12.h"
 #include "dawn_native/d3d12/RenderPipelineD3D12.h"
 #include "dawn_native/d3d12/ResourceAllocator.h"
-#include "dawn_native/d3d12/ResourceUploader.h"
 #include "dawn_native/d3d12/SamplerD3D12.h"
 #include "dawn_native/d3d12/ShaderModuleD3D12.h"
+#include "dawn_native/d3d12/StagingBufferD3D12.h"
 #include "dawn_native/d3d12/SwapChainD3D12.h"
 #include "dawn_native/d3d12/TextureD3D12.h"
 
@@ -134,7 +135,7 @@
         mDescriptorHeapAllocator = std::make_unique<DescriptorHeapAllocator>(this);
         mMapRequestTracker = std::make_unique<MapRequestTracker>(this);
         mResourceAllocator = std::make_unique<ResourceAllocator>(this);
-        mResourceUploader = std::make_unique<ResourceUploader>(this);
+        mDynamicUploader = std::make_unique<DynamicUploader>(this);
 
         NextSerial();
     }
@@ -176,10 +177,6 @@
         return mResourceAllocator.get();
     }
 
-    ResourceUploader* Device::GetResourceUploader() {
-        return mResourceUploader.get();
-    }
-
     void Device::OpenCommandList(ComPtr<ID3D12GraphicsCommandList>* commandList) {
         ComPtr<ID3D12GraphicsCommandList>& cmdList = *commandList;
         if (!cmdList) {
@@ -223,6 +220,7 @@
         mDescriptorHeapAllocator->Tick(mCompletedSerial);
         mMapRequestTracker->Tick(mCompletedSerial);
         mUsedComObjectRefs.ClearUpTo(mCompletedSerial);
+        mDynamicUploader->Tick(mCompletedSerial);
         ExecuteCommandLists({});
         NextSerial();
     }
@@ -335,4 +333,33 @@
         mPCIInfo.name = converter.to_bytes(adapterDesc.Description);
     }
 
+    ResultOrError<std::unique_ptr<StagingBufferBase>> Device::CreateStagingBuffer(size_t size) {
+        std::unique_ptr<StagingBufferBase> stagingBuffer =
+            std::make_unique<StagingBuffer>(size, this);
+        return std::move(stagingBuffer);
+    }
+
+    MaybeError Device::CopyFromStagingToBuffer(StagingBufferBase* source,
+                                               uint32_t sourceOffset,
+                                               BufferBase* destination,
+                                               uint32_t destinationOffset,
+                                               uint32_t size) {
+        ToBackend(destination)
+            ->TransitionUsageNow(GetPendingCommandList(), dawn::BufferUsageBit::TransferDst);
+
+        GetPendingCommandList()->CopyBufferRegion(
+            ToBackend(destination)->GetD3D12Resource().Get(), destinationOffset,
+            ToBackend(source)->GetResource(), sourceOffset, size);
+
+        return {};
+    }
+
+    ResultOrError<DynamicUploader*> Device::GetDynamicUploader() const {
+        // TODO(b-brber): Refactor this into device init once moved into DeviceBase.
+        if (mDynamicUploader->IsEmpty()) {
+            DAWN_TRY(mDynamicUploader->CreateAndAppendBuffer(kDefaultUploadBufferSize));
+        }
+        return mDynamicUploader.get();
+    }
+
 }}  // namespace dawn_native::d3d12
diff --git a/src/dawn_native/d3d12/DeviceD3D12.h b/src/dawn_native/d3d12/DeviceD3D12.h
index 8230227..be489d9 100644
--- a/src/dawn_native/d3d12/DeviceD3D12.h
+++ b/src/dawn_native/d3d12/DeviceD3D12.h
@@ -31,7 +31,6 @@
     class MapRequestTracker;
     class PlatformFunctions;
     class ResourceAllocator;
-    class ResourceUploader;
 
     void ASSERT_SUCCESS(HRESULT hr);
 
@@ -61,11 +60,10 @@
         MapRequestTracker* GetMapRequestTracker() const;
         const PlatformFunctions* GetFunctions();
         ResourceAllocator* GetResourceAllocator();
-        ResourceUploader* GetResourceUploader();
 
         void OpenCommandList(ComPtr<ID3D12GraphicsCommandList>* commandList);
         ComPtr<ID3D12GraphicsCommandList> GetPendingCommandList();
-        Serial GetPendingCommandSerial() const;
+        Serial GetPendingCommandSerial() const override;
 
         void NextSerial();
         void WaitForSerial(Serial serial);
@@ -74,6 +72,15 @@
 
         void ExecuteCommandLists(std::initializer_list<ID3D12CommandList*> commandLists);
 
+        ResultOrError<std::unique_ptr<StagingBufferBase>> CreateStagingBuffer(size_t size) override;
+        MaybeError CopyFromStagingToBuffer(StagingBufferBase* source,
+                                           uint32_t sourceOffset,
+                                           BufferBase* destination,
+                                           uint32_t destinationOffset,
+                                           uint32_t size) override;
+
+        ResultOrError<DynamicUploader*> GetDynamicUploader() const;
+
       private:
         ResultOrError<BindGroupBase*> CreateBindGroupImpl(
             const BindGroupDescriptor* descriptor) override;
@@ -121,9 +128,11 @@
         std::unique_ptr<DescriptorHeapAllocator> mDescriptorHeapAllocator;
         std::unique_ptr<MapRequestTracker> mMapRequestTracker;
         std::unique_ptr<ResourceAllocator> mResourceAllocator;
-        std::unique_ptr<ResourceUploader> mResourceUploader;
+        std::unique_ptr<DynamicUploader> mDynamicUploader;
 
         dawn_native::PCIInfo mPCIInfo;
+
+        static constexpr size_t kDefaultUploadBufferSize = 64000;  // DXGI min heap size is 64kB.
     };
 
 }}  // namespace dawn_native::d3d12
diff --git a/src/dawn_native/d3d12/Forward.h b/src/dawn_native/d3d12/Forward.h
index d4fa333..d803900 100644
--- a/src/dawn_native/d3d12/Forward.h
+++ b/src/dawn_native/d3d12/Forward.h
@@ -33,6 +33,7 @@
     class RenderPipeline;
     class Sampler;
     class ShaderModule;
+    class StagingBuffer;
     class SwapChain;
     class Texture;
     class TextureView;
@@ -52,6 +53,7 @@
         using RenderPipelineType = RenderPipeline;
         using SamplerType = Sampler;
         using ShaderModuleType = ShaderModule;
+        using StagingBufferType = StagingBuffer;
         using SwapChainType = SwapChain;
         using TextureType = Texture;
         using TextureViewType = TextureView;
diff --git a/src/dawn_native/d3d12/ResourceUploader.cpp b/src/dawn_native/d3d12/ResourceUploader.cpp
deleted file mode 100644
index 6aa9a6b..0000000
--- a/src/dawn_native/d3d12/ResourceUploader.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright 2017 The Dawn Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "dawn_native/d3d12/ResourceUploader.h"
-
-#include "dawn_native/d3d12/DeviceD3D12.h"
-#include "dawn_native/d3d12/ResourceAllocator.h"
-
-namespace dawn_native { namespace d3d12 {
-
-    ResourceUploader::ResourceUploader(Device* device) : mDevice(device) {
-    }
-
-    void ResourceUploader::BufferSubData(ComPtr<ID3D12Resource> resource,
-                                         uint32_t start,
-                                         uint32_t count,
-                                         const void* data) {
-        // TODO(enga@google.com): Use a handle to a subset of a large ring buffer. On Release,
-        // decrease reference count on the ring buffer and free when 0. Alternatively, the
-        // SerialQueue could be used to track which last point of the ringbuffer is in use, and
-        // start reusing chunks of it that aren't in flight.
-        UploadHandle uploadHandle = GetUploadBuffer(count);
-        memcpy(uploadHandle.mappedBuffer, data, count);
-        mDevice->GetPendingCommandList()->CopyBufferRegion(resource.Get(), start,
-                                                           uploadHandle.resource.Get(), 0, count);
-        Release(uploadHandle);
-    }
-
-    ResourceUploader::UploadHandle ResourceUploader::GetUploadBuffer(uint32_t requiredSize) {
-        // TODO(enga@google.com): This will find or create a mapped buffer of sufficient size and
-        // return a handle to a mapped range
-        D3D12_RESOURCE_DESC resourceDescriptor;
-        resourceDescriptor.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
-        resourceDescriptor.Alignment = 0;
-        resourceDescriptor.Width = requiredSize;
-        resourceDescriptor.Height = 1;
-        resourceDescriptor.DepthOrArraySize = 1;
-        resourceDescriptor.MipLevels = 1;
-        resourceDescriptor.Format = DXGI_FORMAT_UNKNOWN;
-        resourceDescriptor.SampleDesc.Count = 1;
-        resourceDescriptor.SampleDesc.Quality = 0;
-        resourceDescriptor.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
-        resourceDescriptor.Flags = D3D12_RESOURCE_FLAG_NONE;
-
-        UploadHandle uploadHandle;
-        uploadHandle.resource = mDevice->GetResourceAllocator()->Allocate(
-            D3D12_HEAP_TYPE_UPLOAD, resourceDescriptor, D3D12_RESOURCE_STATE_GENERIC_READ);
-        D3D12_RANGE readRange;
-        readRange.Begin = 0;
-        readRange.End = 0;
-
-        uploadHandle.resource->Map(0, &readRange,
-                                   reinterpret_cast<void**>(&uploadHandle.mappedBuffer));
-        return uploadHandle;
-    }
-
-    void ResourceUploader::Release(UploadHandle uploadHandle) {
-        uploadHandle.resource->Unmap(0, nullptr);
-        mDevice->GetResourceAllocator()->Release(uploadHandle.resource);
-    }
-
-}}  // namespace dawn_native::d3d12
diff --git a/src/dawn_native/d3d12/ResourceUploader.h b/src/dawn_native/d3d12/ResourceUploader.h
deleted file mode 100644
index c3307e5..0000000
--- a/src/dawn_native/d3d12/ResourceUploader.h
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2017 The Dawn Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef DAWNNATIVE_D3D12_RESOURCEUPLOADER_H_
-#define DAWNNATIVE_D3D12_RESOURCEUPLOADER_H_
-
-#include "dawn_native/d3d12/d3d12_platform.h"
-
-#include "dawn_native/Forward.h"
-
-namespace dawn_native { namespace d3d12 {
-
-    class Device;
-
-    class ResourceUploader {
-      public:
-        ResourceUploader(Device* device);
-
-        void BufferSubData(ComPtr<ID3D12Resource> resource,
-                           uint32_t start,
-                           uint32_t count,
-                           const void* data);
-
-      private:
-        struct UploadHandle {
-            ComPtr<ID3D12Resource> resource;
-            uint8_t* mappedBuffer;
-        };
-
-        UploadHandle GetUploadBuffer(uint32_t requiredSize);
-        void Release(UploadHandle uploadHandle);
-
-        Device* mDevice;
-    };
-}}  // namespace dawn_native::d3d12
-
-#endif  // DAWNNATIVE_D3D12_RESOURCEUPLOADER_H_
diff --git a/src/dawn_native/d3d12/StagingBufferD3D12.cpp b/src/dawn_native/d3d12/StagingBufferD3D12.cpp
new file mode 100644
index 0000000..bb711f4
--- /dev/null
+++ b/src/dawn_native/d3d12/StagingBufferD3D12.cpp
@@ -0,0 +1,63 @@
+// Copyright 2018 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "dawn_native/d3d12/StagingBufferD3D12.h"
+#include "dawn_native/d3d12/DeviceD3D12.h"
+#include "dawn_native/d3d12/ResourceAllocator.h"
+
+namespace dawn_native { namespace d3d12 {
+
+    StagingBuffer::StagingBuffer(size_t size, Device* device)
+        : StagingBufferBase(size), mDevice(device) {
+    }
+
+    MaybeError StagingBuffer::Initialize() {
+        D3D12_RESOURCE_DESC resourceDescriptor;
+        resourceDescriptor.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
+        resourceDescriptor.Alignment = 0;
+        resourceDescriptor.Width = GetSize();
+        resourceDescriptor.Height = 1;
+        resourceDescriptor.DepthOrArraySize = 1;
+        resourceDescriptor.MipLevels = 1;
+        resourceDescriptor.Format = DXGI_FORMAT_UNKNOWN;
+        resourceDescriptor.SampleDesc.Count = 1;
+        resourceDescriptor.SampleDesc.Quality = 0;
+        resourceDescriptor.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
+        resourceDescriptor.Flags = D3D12_RESOURCE_FLAG_NONE;
+
+        mUploadHeap = mDevice->GetResourceAllocator()->Allocate(
+            D3D12_HEAP_TYPE_UPLOAD, resourceDescriptor, D3D12_RESOURCE_STATE_GENERIC_READ);
+
+        // TODO(b-brber): Record the GPU pointer for generic non-upload usage.
+
+        if (FAILED(mUploadHeap->Map(0, nullptr, &mMappedPointer))) {
+            return DAWN_CONTEXT_LOST_ERROR("Unable to map staging buffer.");
+        }
+
+        return {};
+    }
+
+    StagingBuffer::~StagingBuffer() {
+        // Invalidate the CPU virtual address & flush cache (if needed).
+        mUploadHeap->Unmap(0, nullptr);
+        mMappedPointer = nullptr;
+
+        mDevice->GetResourceAllocator()->Release(mUploadHeap);
+    }
+
+    ID3D12Resource* StagingBuffer::GetResource() const {
+        return mUploadHeap.Get();
+    }
+
+}}  // namespace dawn_native::d3d12
\ No newline at end of file
diff --git a/src/dawn_native/d3d12/StagingBufferD3D12.h b/src/dawn_native/d3d12/StagingBufferD3D12.h
new file mode 100644
index 0000000..b689df4
--- /dev/null
+++ b/src/dawn_native/d3d12/StagingBufferD3D12.h
@@ -0,0 +1,40 @@
+// Copyright 2018 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef DAWNNATIVE_STAGINGBUFFERD3D12_H_
+#define DAWNNATIVE_STAGINGBUFFERD3D12_H_
+
+#include "dawn_native/StagingBuffer.h"
+#include "dawn_native/d3d12/d3d12_platform.h"
+
+namespace dawn_native { namespace d3d12 {
+
+    class Device;
+
+    class StagingBuffer : public StagingBufferBase {
+      public:
+        StagingBuffer(size_t size, Device* device);
+        ~StagingBuffer();
+
+        ID3D12Resource* GetResource() const;
+
+        MaybeError Initialize() override;
+
+      private:
+        Device* mDevice;
+        ComPtr<ID3D12Resource> mUploadHeap;
+    };
+}}  // namespace dawn_native::d3d12
+
+#endif  // DAWNNATIVE_STAGINGBUFFERD3D12_H_
diff --git a/src/dawn_native/metal/BufferMTL.h b/src/dawn_native/metal/BufferMTL.h
index a56af33..5e65f83 100644
--- a/src/dawn_native/metal/BufferMTL.h
+++ b/src/dawn_native/metal/BufferMTL.h
@@ -34,7 +34,7 @@
         void OnMapCommandSerialFinished(uint32_t mapSerial, uint32_t offset, bool isWrite);
 
       private:
-        void SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
+        MaybeError SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
         void MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void MapWriteAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void UnmapImpl() override;
diff --git a/src/dawn_native/metal/BufferMTL.mm b/src/dawn_native/metal/BufferMTL.mm
index d993e78..85c4272 100644
--- a/src/dawn_native/metal/BufferMTL.mm
+++ b/src/dawn_native/metal/BufferMTL.mm
@@ -49,9 +49,10 @@
         }
     }
 
-    void Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
+    MaybeError Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
         auto* uploader = ToBackend(GetDevice())->GetResourceUploader();
         uploader->BufferSubData(mMtlBuffer, start, count, data);
+        return {};
     }
 
     void Buffer::MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t) {
diff --git a/src/dawn_native/metal/DeviceMTL.h b/src/dawn_native/metal/DeviceMTL.h
index 434ffa8..d250e90 100644
--- a/src/dawn_native/metal/DeviceMTL.h
+++ b/src/dawn_native/metal/DeviceMTL.h
@@ -52,12 +52,19 @@
         id<MTLDevice> GetMTLDevice();
 
         id<MTLCommandBuffer> GetPendingCommandBuffer();
-        Serial GetPendingCommandSerial() const;
+        Serial GetPendingCommandSerial() const override;
         void SubmitPendingCommandBuffer();
 
         MapRequestTracker* GetMapTracker() const;
         ResourceUploader* GetResourceUploader() const;
 
+        ResultOrError<std::unique_ptr<StagingBufferBase>> CreateStagingBuffer(size_t size) override;
+        MaybeError CopyFromStagingToBuffer(StagingBufferBase* source,
+                                           uint32_t sourceOffset,
+                                           BufferBase* destination,
+                                           uint32_t destinationOffset,
+                                           uint32_t size) override;
+
       private:
         ResultOrError<BindGroupBase*> CreateBindGroupImpl(
             const BindGroupDescriptor* descriptor) override;
diff --git a/src/dawn_native/metal/DeviceMTL.mm b/src/dawn_native/metal/DeviceMTL.mm
index f71fe9a..f6b4ab4 100644
--- a/src/dawn_native/metal/DeviceMTL.mm
+++ b/src/dawn_native/metal/DeviceMTL.mm
@@ -17,6 +17,7 @@
 #include "dawn_native/BackendConnection.h"
 #include "dawn_native/BindGroup.h"
 #include "dawn_native/BindGroupLayout.h"
+#include "dawn_native/DynamicUploader.h"
 #include "dawn_native/RenderPassDescriptor.h"
 #include "dawn_native/metal/BufferMTL.h"
 #include "dawn_native/metal/CommandBufferMTL.h"
@@ -297,4 +298,16 @@
         mPCIInfo.name = std::string([mMtlDevice.name UTF8String]);
     }
 
+    ResultOrError<std::unique_ptr<StagingBufferBase>> Device::CreateStagingBuffer(size_t size) {
+        return DAWN_UNIMPLEMENTED_ERROR("Device unable to create staging buffer.");
+    }
+
+    MaybeError Device::CopyFromStagingToBuffer(StagingBufferBase* source,
+                                               uint32_t sourceOffset,
+                                               BufferBase* destination,
+                                               uint32_t destinationOffset,
+                                               uint32_t size) {
+        return DAWN_UNIMPLEMENTED_ERROR("Device unable to copy from staging buffer.");
+    }
+
 }}  // namespace dawn_native::metal
diff --git a/src/dawn_native/null/DeviceNull.cpp b/src/dawn_native/null/DeviceNull.cpp
index 7244435..4f765ba 100644
--- a/src/dawn_native/null/DeviceNull.cpp
+++ b/src/dawn_native/null/DeviceNull.cpp
@@ -16,6 +16,7 @@
 
 #include "dawn_native/BackendConnection.h"
 #include "dawn_native/Commands.h"
+#include "dawn_native/DynamicUploader.h"
 
 #include <spirv-cross/spirv_cross.hpp>
 
@@ -57,6 +58,7 @@
     // Device
 
     Device::Device(Adapter* adapter) : DeviceBase(adapter) {
+        mDynamicUploader = std::make_unique<DynamicUploader>(this);
     }
 
     Device::~Device() {
@@ -122,6 +124,20 @@
         return new TextureView(texture, descriptor);
     }
 
+    ResultOrError<std::unique_ptr<StagingBufferBase>> Device::CreateStagingBuffer(size_t size) {
+        std::unique_ptr<StagingBufferBase> stagingBuffer =
+            std::make_unique<StagingBuffer>(size, this);
+        return std::move(stagingBuffer);
+    }
+
+    MaybeError Device::CopyFromStagingToBuffer(StagingBufferBase* source,
+                                               uint32_t sourceOffset,
+                                               BufferBase* destination,
+                                               uint32_t destinationOffset,
+                                               uint32_t size) {
+        return DAWN_UNIMPLEMENTED_ERROR("Device unable to copy from staging buffer.");
+    }
+
     Serial Device::GetCompletedCommandSerial() const {
         return mCompletedSerial;
     }
@@ -130,6 +146,10 @@
         return mLastSubmittedSerial;
     }
 
+    Serial Device::GetPendingCommandSerial() const {
+        return mLastSubmittedSerial + 1;
+    }
+
     void Device::TickImpl() {
         SubmitPendingOperations();
     }
@@ -179,10 +199,11 @@
         }
     }
 
-    void Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
+    MaybeError Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
         ASSERT(start + count <= GetSize());
         ASSERT(mBackingData);
         memcpy(mBackingData.get() + start, data, count);
+        return {};
     }
 
     void Buffer::MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) {
@@ -272,4 +293,15 @@
         return dawn::TextureFormat::R8G8B8A8Unorm;
     }
 
+    // StagingBuffer
+
+    StagingBuffer::StagingBuffer(size_t size, Device* device) : StagingBufferBase(size) {
+    }
+
+    MaybeError StagingBuffer::Initialize() {
+        mBuffer = std::make_unique<uint8_t[]>(GetSize());
+        mMappedPointer = mBuffer.get();
+        return {};
+    }
+
 }}  // namespace dawn_native::null
diff --git a/src/dawn_native/null/DeviceNull.h b/src/dawn_native/null/DeviceNull.h
index 03f8967..fd60126 100644
--- a/src/dawn_native/null/DeviceNull.h
+++ b/src/dawn_native/null/DeviceNull.h
@@ -26,8 +26,10 @@
 #include "dawn_native/Queue.h"
 #include "dawn_native/RenderPassDescriptor.h"
 #include "dawn_native/RenderPipeline.h"
+#include "dawn_native/RingBuffer.h"
 #include "dawn_native/Sampler.h"
 #include "dawn_native/ShaderModule.h"
+#include "dawn_native/StagingBuffer.h"
 #include "dawn_native/SwapChain.h"
 #include "dawn_native/Texture.h"
 #include "dawn_native/ToBackend.h"
@@ -96,11 +98,19 @@
 
         Serial GetCompletedCommandSerial() const final override;
         Serial GetLastSubmittedCommandSerial() const final override;
+        Serial GetPendingCommandSerial() const override;
         void TickImpl() override;
 
         void AddPendingOperation(std::unique_ptr<PendingOperation> operation);
         void SubmitPendingOperations();
 
+        ResultOrError<std::unique_ptr<StagingBufferBase>> CreateStagingBuffer(size_t size) override;
+        MaybeError CopyFromStagingToBuffer(StagingBufferBase* source,
+                                           uint32_t sourceOffset,
+                                           BufferBase* destination,
+                                           uint32_t destinationOffset,
+                                           uint32_t size) override;
+
       private:
         ResultOrError<BindGroupBase*> CreateBindGroupImpl(
             const BindGroupDescriptor* descriptor) override;
@@ -125,6 +135,7 @@
         Serial mCompletedSerial = 0;
         Serial mLastSubmittedSerial = 0;
         std::vector<std::unique_ptr<PendingOperation>> mPendingOperations;
+        std::unique_ptr<DynamicUploader> mDynamicUploader;
     };
 
     class Buffer : public BufferBase {
@@ -135,7 +146,7 @@
         void MapReadOperationCompleted(uint32_t serial, void* ptr, bool isWrite);
 
       private:
-        void SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
+        MaybeError SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
         void MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void MapWriteAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void UnmapImpl() override;
@@ -186,6 +197,15 @@
         dawn::TextureFormat GetPreferredFormat() const;
     };
 
+    class StagingBuffer : public StagingBufferBase {
+      public:
+        StagingBuffer(size_t size, Device* device);
+        MaybeError Initialize() override;
+
+      private:
+        std::unique_ptr<uint8_t[]> mBuffer;
+    };
+
 }}  // namespace dawn_native::null
 
 #endif  // DAWNNATIVE_NULL_DEVICENULL_H_
diff --git a/src/dawn_native/opengl/BufferGL.cpp b/src/dawn_native/opengl/BufferGL.cpp
index e14307f..ed9aa53 100644
--- a/src/dawn_native/opengl/BufferGL.cpp
+++ b/src/dawn_native/opengl/BufferGL.cpp
@@ -31,9 +31,10 @@
         return mBuffer;
     }
 
-    void Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
+    MaybeError Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
         glBindBuffer(GL_ARRAY_BUFFER, mBuffer);
         glBufferSubData(GL_ARRAY_BUFFER, start, count, data);
+        return {};
     }
 
     void Buffer::MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) {
diff --git a/src/dawn_native/opengl/BufferGL.h b/src/dawn_native/opengl/BufferGL.h
index 497beec..0f35484 100644
--- a/src/dawn_native/opengl/BufferGL.h
+++ b/src/dawn_native/opengl/BufferGL.h
@@ -30,7 +30,7 @@
         GLuint GetHandle() const;
 
       private:
-        void SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
+        MaybeError SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
         void MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void MapWriteAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void UnmapImpl() override;
diff --git a/src/dawn_native/opengl/DeviceGL.cpp b/src/dawn_native/opengl/DeviceGL.cpp
index 934a9b6..f1f80ed 100644
--- a/src/dawn_native/opengl/DeviceGL.cpp
+++ b/src/dawn_native/opengl/DeviceGL.cpp
@@ -17,6 +17,7 @@
 #include "dawn_native/BackendConnection.h"
 #include "dawn_native/BindGroup.h"
 #include "dawn_native/BindGroupLayout.h"
+#include "dawn_native/DynamicUploader.h"
 #include "dawn_native/RenderPassDescriptor.h"
 #include "dawn_native/opengl/BufferGL.h"
 #include "dawn_native/opengl/CommandBufferGL.h"
@@ -115,6 +116,10 @@
         return mLastSubmittedSerial;
     }
 
+    Serial Device::GetPendingCommandSerial() const {
+        return mLastSubmittedSerial + 1;
+    }
+
     void Device::TickImpl() {
         CheckPassedFences();
     }
@@ -144,4 +149,16 @@
         }
     }
 
+    ResultOrError<std::unique_ptr<StagingBufferBase>> Device::CreateStagingBuffer(size_t size) {
+        return DAWN_UNIMPLEMENTED_ERROR("Device unable to create staging buffer.");
+    }
+
+    MaybeError Device::CopyFromStagingToBuffer(StagingBufferBase* source,
+                                               uint32_t sourceOffset,
+                                               BufferBase* destination,
+                                               uint32_t destinationOffset,
+                                               uint32_t size) {
+        return DAWN_UNIMPLEMENTED_ERROR("Device unable to copy from staging buffer.");
+    }
+
 }}  // namespace dawn_native::opengl
diff --git a/src/dawn_native/opengl/DeviceGL.h b/src/dawn_native/opengl/DeviceGL.h
index 192b9f2..8c15162 100644
--- a/src/dawn_native/opengl/DeviceGL.h
+++ b/src/dawn_native/opengl/DeviceGL.h
@@ -48,8 +48,16 @@
 
         Serial GetCompletedCommandSerial() const final override;
         Serial GetLastSubmittedCommandSerial() const final override;
+        Serial GetPendingCommandSerial() const override;
         void TickImpl() override;
 
+        ResultOrError<std::unique_ptr<StagingBufferBase>> CreateStagingBuffer(size_t size) override;
+        MaybeError CopyFromStagingToBuffer(StagingBufferBase* source,
+                                           uint32_t sourceOffset,
+                                           BufferBase* destination,
+                                           uint32_t destinationOffset,
+                                           uint32_t size) override;
+
       private:
         ResultOrError<BindGroupBase*> CreateBindGroupImpl(
             const BindGroupDescriptor* descriptor) override;
diff --git a/src/dawn_native/vulkan/BufferVk.cpp b/src/dawn_native/vulkan/BufferVk.cpp
index 73e1538..4175097 100644
--- a/src/dawn_native/vulkan/BufferVk.cpp
+++ b/src/dawn_native/vulkan/BufferVk.cpp
@@ -196,7 +196,7 @@
         mLastUsage = usage;
     }
 
-    void Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
+    MaybeError Buffer::SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) {
         Device* device = ToBackend(GetDevice());
 
         VkCommandBuffer commands = device->GetPendingCommandBuffer();
@@ -204,6 +204,7 @@
 
         BufferUploader* uploader = device->GetBufferUploader();
         uploader->BufferSubData(mHandle, start, count, data);
+        return {};
     }
 
     void Buffer::MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t /*count*/) {
diff --git a/src/dawn_native/vulkan/BufferVk.h b/src/dawn_native/vulkan/BufferVk.h
index 41ef9c4..ae90b4f 100644
--- a/src/dawn_native/vulkan/BufferVk.h
+++ b/src/dawn_native/vulkan/BufferVk.h
@@ -41,7 +41,7 @@
         void TransitionUsageNow(VkCommandBuffer commands, dawn::BufferUsageBit usage);
 
       private:
-        void SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
+        MaybeError SetSubDataImpl(uint32_t start, uint32_t count, const uint8_t* data) override;
         void MapReadAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void MapWriteAsyncImpl(uint32_t serial, uint32_t start, uint32_t count) override;
         void UnmapImpl() override;
diff --git a/src/dawn_native/vulkan/DeviceVk.cpp b/src/dawn_native/vulkan/DeviceVk.cpp
index da23c0a..d42df54 100644
--- a/src/dawn_native/vulkan/DeviceVk.cpp
+++ b/src/dawn_native/vulkan/DeviceVk.cpp
@@ -17,6 +17,7 @@
 #include "common/Platform.h"
 #include "dawn_native/BackendConnection.h"
 #include "dawn_native/Commands.h"
+#include "dawn_native/DynamicUploader.h"
 #include "dawn_native/ErrorData.h"
 #include "dawn_native/vulkan/BindGroupLayoutVk.h"
 #include "dawn_native/vulkan/BindGroupVk.h"
@@ -682,4 +683,16 @@
         commands->commandBuffer = VK_NULL_HANDLE;
     }
 
+    ResultOrError<std::unique_ptr<StagingBufferBase>> Device::CreateStagingBuffer(size_t size) {
+        return DAWN_UNIMPLEMENTED_ERROR("Device unable to create staging buffer.");
+    }
+
+    MaybeError Device::CopyFromStagingToBuffer(StagingBufferBase* source,
+                                               uint32_t sourceOffset,
+                                               BufferBase* destination,
+                                               uint32_t destinationOffset,
+                                               uint32_t size) {
+        return DAWN_UNIMPLEMENTED_ERROR("Device unable to copy from staging buffer.");
+    }
+
 }}  // namespace dawn_native::vulkan
diff --git a/src/dawn_native/vulkan/DeviceVk.h b/src/dawn_native/vulkan/DeviceVk.h
index b3af48a..27139cb 100644
--- a/src/dawn_native/vulkan/DeviceVk.h
+++ b/src/dawn_native/vulkan/DeviceVk.h
@@ -58,7 +58,7 @@
         RenderPassCache* GetRenderPassCache() const;
 
         VkCommandBuffer GetPendingCommandBuffer();
-        Serial GetPendingCommandSerial() const;
+        Serial GetPendingCommandSerial() const override;
         void SubmitPendingCommands();
         void AddWaitSemaphore(VkSemaphore semaphore);
 
@@ -75,6 +75,13 @@
 
         const dawn_native::PCIInfo& GetPCIInfo() const override;
 
+        ResultOrError<std::unique_ptr<StagingBufferBase>> CreateStagingBuffer(size_t size) override;
+        MaybeError CopyFromStagingToBuffer(StagingBufferBase* source,
+                                           uint32_t sourceOffset,
+                                           BufferBase* destination,
+                                           uint32_t destinationOffset,
+                                           uint32_t size) override;
+
       private:
         ResultOrError<BindGroupBase*> CreateBindGroupImpl(
             const BindGroupDescriptor* descriptor) override;
diff --git a/src/tests/end2end/BufferTests.cpp b/src/tests/end2end/BufferTests.cpp
index f1e4752..70dfac8 100644
--- a/src/tests/end2end/BufferTests.cpp
+++ b/src/tests/end2end/BufferTests.cpp
@@ -222,10 +222,15 @@
 TEST_P(BufferSetSubDataTests, ManySetSubData) {
     // TODO(cwallez@chromium.org): Use ringbuffers for SetSubData on explicit APIs.
     // otherwise this creates too many resources and can take freeze the driver(?)
-    DAWN_SKIP_TEST_IF(IsD3D12() || IsMetal() || IsVulkan());
+    DAWN_SKIP_TEST_IF(IsMetal() || IsVulkan());
 
+    // Note: Increasing the size of the buffer will likely cause timeout issues.
+    // In D3D12, timeout detection occurs when the GPU scheduler tries but cannot preempt the task
+    // executing these commands in-flight. If this takes longer than ~2s, a device reset occurs and
+    // fails the test. Since GPUs may or may not complete by then, this test must be disabled OR
+    // modified to be well-below the timeout limit.
     constexpr uint32_t kSize = 4000 * 1000;
-    constexpr uint32_t kElements = 1000 * 1000;
+    constexpr uint32_t kElements = 500 * 500;
     dawn::BufferDescriptor descriptor;
     descriptor.size = kSize;
     descriptor.usage = dawn::BufferUsageBit::TransferSrc | dawn::BufferUsageBit::TransferDst;
diff --git a/src/tests/unittests/RingBufferTests.cpp b/src/tests/unittests/RingBufferTests.cpp
new file mode 100644
index 0000000..1b8fc7b
--- /dev/null
+++ b/src/tests/unittests/RingBufferTests.cpp
@@ -0,0 +1,201 @@
+// Copyright 2018 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <gtest/gtest.h>
+
+#include "dawn_native/null/DeviceNull.h"
+
+using namespace dawn_native;
+
+namespace {
+
+    size_t ValidateValidUploadHandle(const UploadHandle& uploadHandle) {
+        ASSERT(uploadHandle.mappedBuffer != nullptr);
+        return uploadHandle.startOffset;
+    }
+
+    void ValidateInvalidUploadHandle(const UploadHandle& uploadHandle) {
+        ASSERT_EQ(uploadHandle.mappedBuffer, nullptr);
+    }
+}  // namespace
+
+class RingBufferTests : public testing::Test {
+  protected:
+    void SetUp() override {
+        // TODO(b-brber): Create this device through the adapter.
+        mDevice = std::make_unique<null::Device>(/*adapter*/ nullptr);
+    }
+
+    null::Device* GetDevice() const {
+        return mDevice.get();
+    }
+
+    std::unique_ptr<RingBuffer> CreateRingBuffer(size_t size) {
+        std::unique_ptr<RingBuffer> ringBuffer = std::make_unique<RingBuffer>(mDevice.get(), size);
+        DAWN_UNUSED(ringBuffer->Initialize());
+        return ringBuffer;
+    }
+
+  private:
+    std::unique_ptr<null::Device> mDevice;
+};
+
+// Number of basic tests for Ringbuffer
+TEST_F(RingBufferTests, BasicTest) {
+    constexpr size_t sizeInBytes = 64000;
+    std::unique_ptr<RingBuffer> buffer = CreateRingBuffer(sizeInBytes);
+
+    // Ensure no requests exist on empty buffer.
+    EXPECT_TRUE(buffer->Empty());
+
+    ASSERT_EQ(buffer->GetSize(), sizeInBytes);
+
+    // Ensure failure upon sub-allocating an oversized request.
+    ValidateInvalidUploadHandle(buffer->SubAllocate(sizeInBytes + 1));
+
+    // Fill the entire buffer with two requests of equal size.
+    ValidateValidUploadHandle(buffer->SubAllocate(sizeInBytes / 2));
+    ValidateValidUploadHandle(buffer->SubAllocate(sizeInBytes / 2));
+
+    // Ensure the buffer is full.
+    ValidateInvalidUploadHandle(buffer->SubAllocate(1));
+}
+
+// Tests that several ringbuffer allocations do not fail.
+TEST_F(RingBufferTests, RingBufferManyAlloc) {
+    constexpr size_t maxNumOfFrames = 64000;
+    constexpr size_t frameSizeInBytes = 4;
+
+    std::unique_ptr<RingBuffer> buffer = CreateRingBuffer(maxNumOfFrames * frameSizeInBytes);
+
+    size_t offset = 0;
+    for (size_t i = 0; i < maxNumOfFrames; ++i) {
+        offset = ValidateValidUploadHandle(buffer->SubAllocate(frameSizeInBytes));
+        GetDevice()->Tick();
+        ASSERT_EQ(offset, i * frameSizeInBytes);
+    }
+}
+
+// Tests ringbuffer sub-allocations of the same serial are correctly tracked.
+TEST_F(RingBufferTests, AllocInSameFrame) {
+    constexpr size_t maxNumOfFrames = 3;
+    constexpr size_t frameSizeInBytes = 4;
+
+    std::unique_ptr<RingBuffer> buffer = CreateRingBuffer(maxNumOfFrames * frameSizeInBytes);
+
+    //    F1
+    //  [xxxx|--------]
+
+    ValidateValidUploadHandle(buffer->SubAllocate(frameSizeInBytes));
+    GetDevice()->Tick();
+
+    //    F1   F2
+    //  [xxxx|xxxx|----]
+
+    ValidateValidUploadHandle(buffer->SubAllocate(frameSizeInBytes));
+
+    //    F1     F2
+    //  [xxxx|xxxxxxxx]
+
+    size_t offset = ValidateValidUploadHandle(buffer->SubAllocate(frameSizeInBytes));
+
+    ASSERT_EQ(offset, 8u);
+    ASSERT_EQ(buffer->GetUsedSize(), frameSizeInBytes * 3);
+
+    buffer->Tick(1);
+
+    // Used size does not change as previous sub-allocations were not tracked.
+    ASSERT_EQ(buffer->GetUsedSize(), frameSizeInBytes * 3);
+
+    buffer->Tick(2);
+
+    ASSERT_EQ(buffer->GetUsedSize(), 0u);
+    EXPECT_TRUE(buffer->Empty());
+}
+
+// Tests ringbuffer sub-allocation at various offsets.
+TEST_F(RingBufferTests, RingBufferSubAlloc) {
+    constexpr size_t maxNumOfFrames = 10;
+    constexpr size_t frameSizeInBytes = 4;
+
+    std::unique_ptr<RingBuffer> buffer = CreateRingBuffer(maxNumOfFrames * frameSizeInBytes);
+
+    // Sub-alloc the first eight frames.
+    for (size_t i = 0; i < 8; ++i) {
+        ValidateValidUploadHandle(buffer->SubAllocate(frameSizeInBytes));
+        buffer->Track();
+        GetDevice()->Tick();
+    }
+
+    // Each frame corrresponds to the serial number (for simplicity).
+    //
+    //    F1   F2   F3   F4   F5   F6   F7   F8
+    //  [xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxx|--------]
+    //
+
+    // Ensure an oversized allocation fails (only 8 bytes left)
+    ValidateInvalidUploadHandle(buffer->SubAllocate(frameSizeInBytes * 3));
+    ASSERT_EQ(buffer->GetUsedSize(), frameSizeInBytes * 8);
+
+    // Reclaim the first 3 frames.
+    buffer->Tick(3);
+
+    //                 F4   F5   F6   F7   F8
+    //  [------------|xxxx|xxxx|xxxx|xxxx|xxxx|--------]
+    //
+    ASSERT_EQ(buffer->GetUsedSize(), frameSizeInBytes * 5);
+
+    // Re-try the over-sized allocation.
+    size_t offset = ValidateValidUploadHandle(buffer->SubAllocate(frameSizeInBytes * 3));
+
+    //        F9       F4   F5   F6   F7   F8
+    //  [xxxxxxxxxxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxxxxxx]
+    //                                         ^^^^^^^^ wasted
+
+    // In this example, Tick(8) could not reclaim the wasted bytes. The wasted bytes
+    // were add to F9's sub-allocation.
+    // TODO(b-brber): Decide if Tick(8) should free these wasted bytes.
+
+    ASSERT_EQ(offset, 0u);
+    ASSERT_EQ(buffer->GetUsedSize(), frameSizeInBytes * maxNumOfFrames);
+
+    // Ensure we are full.
+    ValidateInvalidUploadHandle(buffer->SubAllocate(frameSizeInBytes));
+
+    // Reclaim the next two frames.
+    buffer->Tick(5);
+
+    //        F9       F4   F5   F6   F7   F8
+    //  [xxxxxxxxxxxx|----|----|xxxx|xxxx|xxxx|xxxxxxxx]
+    //
+    ASSERT_EQ(buffer->GetUsedSize(), frameSizeInBytes * 8);
+
+    // Sub-alloc the chunk in the middle.
+    offset = ValidateValidUploadHandle(buffer->SubAllocate(frameSizeInBytes * 2));
+
+    ASSERT_EQ(offset, frameSizeInBytes * 3);
+    ASSERT_EQ(buffer->GetUsedSize(), frameSizeInBytes * maxNumOfFrames);
+
+    //        F9                 F6   F7   F8
+    //  [xxxxxxxxxxxx|xxxx|xxxx|xxxx|xxxx|xxxx|xxxxxxxx]
+    //                ^^^^^^^^^ untracked
+
+    // Ensure we are full.
+    ValidateInvalidUploadHandle(buffer->SubAllocate(frameSizeInBytes));
+
+    // Reclaim all.
+    buffer->Tick(maxNumOfFrames);
+
+    EXPECT_TRUE(buffer->Empty());
+}
diff --git a/src/tests/unittests/SerialQueueTests.cpp b/src/tests/unittests/SerialQueueTests.cpp
index 712c093..6f75eb7 100644
--- a/src/tests/unittests/SerialQueueTests.cpp
+++ b/src/tests/unittests/SerialQueueTests.cpp
@@ -95,6 +95,7 @@
         expectedValues.erase(expectedValues.begin());
     }
     ASSERT_TRUE(expectedValues.empty());
+    EXPECT_EQ(queue.LastSerial(), 2u);
 }
 
 // Test ClearUpTo
@@ -110,6 +111,7 @@
     queue.Enqueue(vector3, 1);
 
     queue.ClearUpTo(0);
+    EXPECT_EQ(queue.LastSerial(), 1u);
 
     std::vector<int> expectedValues = {9, 0};
     for (int value : queue.IterateAll()) {
@@ -141,3 +143,14 @@
     queue.Enqueue(vector1, 6);
     EXPECT_EQ(queue.FirstSerial(), 6u);
 }
+
+// Test LastSerial
+TEST(SerialQueue, LastSerial) {
+    TestSerialQueue queue;
+
+    queue.Enqueue({1}, 0);
+    EXPECT_EQ(queue.LastSerial(), 0u);
+
+    queue.Enqueue({2}, 1);
+    EXPECT_EQ(queue.LastSerial(), 1u);
+}
\ No newline at end of file