use coherent memory for command buffers

If vulkan aux memory feature is enabled:
- ResourceTracker  uses custom alloc/free functions using coherent memory
- CommandBufferStagingStream(s) in ResourceTracker to use custom allocation
- Flush command buffers to aux memory
- abort()if allocBuffer() is called while previous data read is incomplete
    - add new test for this behavior
    - Test results: https://paste.googleplex.com/5119004271181824

Tested with `flat run emulator` and verified command buffers are being flushed to aux memory and decoded on host

Tested with `full-gles3` and `full-vulkan` benchmarking app

Change-Id: I508d6f6fa6e29386b0764b5f3f78b5159396a260
(cherry picked from commit f7beff89d41bf8ced82e8554819bd00f4132c642)
diff --git a/system/vulkan_enc/CommandBufferStagingStream.cpp b/system/vulkan_enc/CommandBufferStagingStream.cpp
index faee5bf..8487bcd 100644
--- a/system/vulkan_enc/CommandBufferStagingStream.cpp
+++ b/system/vulkan_enc/CommandBufferStagingStream.cpp
@@ -38,13 +38,13 @@
     // use default allocators
     m_alloc = [](size_t size) -> Memory {
         return {
-            .deviceMemory = nullptr,  // no device memory for malloc
+            .deviceMemory = VK_NULL_HANDLE,  // no device memory for malloc
             .ptr = malloc(size),
         };
     };
     m_free = [](const Memory& mem) { free(mem.ptr); };
     m_realloc = [](const Memory& mem, size_t size) -> Memory {
-        return {.deviceMemory = nullptr, .ptr = realloc(mem.ptr, size)};
+        return {.deviceMemory = VK_NULL_HANDLE, .ptr = realloc(mem.ptr, size)};
     };
 }
 
@@ -88,7 +88,7 @@
 
     m_free = [&freeFn](const Memory& mem) {
         if (!freeFn) {
-            ALOGE("Custom free for device memory(%p) failed\n", mem.deviceMemory);
+            ALOGE("Custom free for memory(%p) failed\n", mem.ptr);
             return;
         }
         freeFn(mem);
@@ -186,6 +186,18 @@
         return (void*)(getDataPtr() + m_writePos);
     }
 
+    // for custom allocations, host should have finished reading
+    // data from command buffer since command buffers are flushed
+    // on queue submit.
+    // allocBuffer should not be called on command buffers that are currently
+    // being read by the host
+    if (m_usingCustomAlloc) {
+        uint32_t* syncDWordPtr = reinterpret_cast<uint32_t*>(m_mem.ptr);
+        LOG_ALWAYS_FATAL_IF(
+            __atomic_load_n(syncDWordPtr, __ATOMIC_ACQUIRE) != kSyncDataReadComplete,
+            "FATAL: allocBuffer() called but previous read not complete");
+    }
+
     return (void*)(getDataPtr() + m_writePos);
 }
 
@@ -235,3 +247,5 @@
     m_writePos = 0;
     IOStream::rewind();
 }
+
+VkDeviceMemory CommandBufferStagingStream::getDeviceMemory() { return m_mem.deviceMemory; }
\ No newline at end of file
diff --git a/system/vulkan_enc/CommandBufferStagingStream.h b/system/vulkan_enc/CommandBufferStagingStream.h
index 0fb859d..2742e20 100644
--- a/system/vulkan_enc/CommandBufferStagingStream.h
+++ b/system/vulkan_enc/CommandBufferStagingStream.h
@@ -16,9 +16,12 @@
 #ifndef __COMMAND_BUFFER_STAGING_STREAM_H
 #define __COMMAND_BUFFER_STAGING_STREAM_H
 
-#include "IOStream.h"
+#include <vulkan/vulkan_core.h>
+
 #include <functional>
 
+#include "IOStream.h"
+
 class CommandBufferStagingStream : public IOStream {
 public:
  // host will write kSyncDataReadComplete to the sync bytes to indicate memory is no longer being
@@ -31,11 +34,10 @@
  // indicates read is pending
  static constexpr uint32_t kSyncDataReadPending = 0X1;
 
- // alias for DeviceMemory
- using DeviceMemory = void*;
  // \struct backing memory structure
  struct Memory {
-     DeviceMemory deviceMemory = nullptr;  // device memory associated with allocated memory
+     VkDeviceMemory deviceMemory =
+         VK_NULL_HANDLE;                   // device memory associated with allocated memory
      void* ptr = nullptr;                  // pointer to allocated memory
      bool operator==(const Memory& rhs) const {
          return (deviceMemory == rhs.deviceMemory) && (ptr == rhs.ptr);
@@ -75,6 +77,10 @@
  // when not using custom allocators
  void markFlushing();
 
+ // gets the device memory associated with the stream. This is VK_NULL_HANDLE for default allocation
+ // \return device memory
+ VkDeviceMemory getDeviceMemory();
+
 private:
  // underlying memory for data
  Memory m_mem;
diff --git a/system/vulkan_enc/ResourceTracker.cpp b/system/vulkan_enc/ResourceTracker.cpp
index aebcdc0..c5f1985 100644
--- a/system/vulkan_enc/ResourceTracker.cpp
+++ b/system/vulkan_enc/ResourceTracker.cpp
@@ -15,18 +15,15 @@
 
 #include "ResourceTracker.h"
 
-#include "Resources.h"
-#include "CommandBufferStagingStream.h"
-#include "DescriptorSetVirtualization.h"
-
-#include "aemu/base/Optional.h"
-#include "aemu/base/threads/AndroidWorkPool.h"
-#include "aemu/base/Tracing.h"
-
-#include "goldfish_vk_private_defs.h"
-
 #include "../OpenglSystemCommon/EmulatorFeatureInfo.h"
 #include "../OpenglSystemCommon/HostConnection.h"
+#include "CommandBufferStagingStream.h"
+#include "DescriptorSetVirtualization.h"
+#include "Resources.h"
+#include "aemu/base/Optional.h"
+#include "aemu/base/Tracing.h"
+#include "aemu/base/threads/AndroidWorkPool.h"
+#include "goldfish_vk_private_defs.h"
 #include "vulkan/vulkan_core.h"
 
 /// Use installed headers or locally defined Fuchsia-specific bits
@@ -205,6 +202,14 @@
     Lock mLock;
     std::vector<CommandBufferStagingStream*> streams;
     std::vector<VkEncoder*> encoders;
+    /// \brief sets alloc and free callbacks for memory allocation for CommandBufferStagingStream(s)
+    /// \param allocFn is the callback to allocate memory
+    /// \param freeFn is the callback to free memory
+    void setAllocFree(CommandBufferStagingStream::Alloc&& allocFn,
+                      CommandBufferStagingStream::Free&& freeFn) {
+        mAlloc = allocFn;
+        mFree = freeFn;
+    }
 
     ~StagingInfo() {
         for (auto stream : streams) {
@@ -228,7 +233,12 @@
         CommandBufferStagingStream* stream;
         VkEncoder* encoder;
         if (streams.empty()) {
-            stream = new CommandBufferStagingStream;
+            if (mAlloc && mFree) {
+                // if custom allocators are provided, forward them to CommandBufferStagingStream
+                stream = new CommandBufferStagingStream(mAlloc, mFree);
+            } else {
+                stream = new CommandBufferStagingStream;
+            }
             encoder = new VkEncoder(stream);
         } else {
             stream = streams.back();
@@ -239,6 +249,10 @@
         *streamOut = stream;
         *encoderOut = encoder;
     }
+
+   private:
+    CommandBufferStagingStream::Alloc mAlloc = nullptr;
+    CommandBufferStagingStream::Free mFree = nullptr;
 };
 
 static StagingInfo sStaging;
@@ -2933,8 +2947,8 @@
         return coherentMemory;
     }
 
-  VkResult allocateCoherentMemory(VkDevice device, const VkMemoryAllocateInfo *pAllocateInfo,
-                                    VkEncoder *enc, VkDeviceMemory* pMemory) {
+    VkResult allocateCoherentMemory(VkDevice device, const VkMemoryAllocateInfo* pAllocateInfo,
+                                    VkEncoder* enc, VkDeviceMemory* pMemory) {
         uint64_t offset = 0;
         uint8_t *ptr = nullptr;
         VkMemoryAllocateFlagsInfo allocFlagsInfo;
@@ -3019,7 +3033,6 @@
 
     VkResult getCoherentMemory(const VkMemoryAllocateInfo* pAllocateInfo, VkEncoder* enc,
                                VkDevice device, VkDeviceMemory* pMemory) {
-
         VkMemoryAllocateFlagsInfo allocFlagsInfo;
         VkMemoryOpaqueCaptureAddressAllocateInfo opaqueCaptureAddressAllocInfo;
 
@@ -3061,6 +3074,9 @@
                 info.coherentMemory = coherentMemory;
                 info.device = device;
 
+                // for suballocated memory, create an alias VkDeviceMemory handle for application
+                // memory used for suballocations will still be VkDeviceMemory associated with
+                // CoherentMemory
                 auto mem = new_from_host_VkDeviceMemory(VK_NULL_HANDLE);
                 info_VkDeviceMemory[mem] = info;
                 *pMemory = mem;
@@ -3731,6 +3747,23 @@
         _RETURN_SCUCCESS_WITH_DEVICE_MEMORY_REPORT;
     }
 
+    CoherentMemoryPtr freeCoherentMemoryLocked(VkDeviceMemory memory, VkDeviceMemory_Info& info) {
+        if (info.coherentMemory && info.ptr) {
+            if (info.coherentMemory->getDeviceMemory() != memory) {
+                delete_goldfish_VkDeviceMemory(memory);
+            }
+
+            if (info.ptr) {
+                info.coherentMemory->release(info.ptr);
+                info.ptr = nullptr;
+            }
+
+            return std::move(info.coherentMemory);
+        }
+
+        return nullptr;
+    }
+
     void on_vkFreeMemory(
         void* context,
         VkDevice device,
@@ -3748,15 +3781,12 @@
             memoryObjectId = getAHardwareBufferId(info.ahw);
         }
 #endif
-        emitDeviceMemoryReport(
-            info_VkDevice[device],
-            info.imported ? VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_UNIMPORT_EXT
-                          : VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT,
-            memoryObjectId,
-            0 /* size */,
-            VK_OBJECT_TYPE_DEVICE_MEMORY,
-            (uint64_t)(void*)memory
-        );
+
+        emitDeviceMemoryReport(info_VkDevice[device],
+                               info.imported ? VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_UNIMPORT_EXT
+                                             : VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT,
+                               memoryObjectId, 0 /* size */, VK_OBJECT_TYPE_DEVICE_MEMORY,
+                               (uint64_t)(void*)memory);
 
 #ifdef VK_USE_PLATFORM_FUCHSIA
         if (info.vmoHandle && info.ptr) {
@@ -3776,23 +3806,13 @@
             return;
         }
 
-        if (info.coherentMemory && info.ptr) {
-            if (info.coherentMemory->getDeviceMemory() != memory) {
-                delete_goldfish_VkDeviceMemory(memory);
-            }
+        auto coherentMemory = freeCoherentMemoryLocked(memory, info);
 
-            if (info.ptr) {
-                info.coherentMemory->release(info.ptr);
-                info.ptr = nullptr;
-            }
-
-            auto coherentMemory = std::move(info.coherentMemory);
-            // We have to release the lock before we could possibly free a
-            // CoherentMemory, because that will call into VkEncoder, which
-            // shouldn't be called when the lock is held.
-            lock.unlock();
-            coherentMemory = nullptr;
-        }
+        // We have to release the lock before we could possibly free a
+        // CoherentMemory, because that will call into VkEncoder, which
+        // shouldn't be called when the lock is held.
+        lock.unlock();
+        coherentMemory = nullptr;
     }
 
     VkResult on_vkMapMemory(
@@ -5716,17 +5736,42 @@
 
             unsigned char* writtenPtr = 0;
             size_t written = 0;
-            ((CommandBufferStagingStream*)cb->privateStream)->getWritten(&writtenPtr, &written);
+            CommandBufferStagingStream* cmdBufStream =
+                static_cast<CommandBufferStagingStream*>(cb->privateStream);
+            cmdBufStream->getWritten(&writtenPtr, &written);
 
             // There's no pending commands here, skip. (case 2, stream created but no new recordings)
             if (!written) continue;
 
             // There are pending commands to flush.
             VkEncoder* enc = (VkEncoder*)context;
-            enc->vkQueueFlushCommandsGOOGLE(queue, cmdbuf, written, (const void*)writtenPtr, true /* do lock */);
+            VkDeviceMemory deviceMemory = cmdBufStream->getDeviceMemory();
+            VkDeviceSize dataOffset = 0;
+            if (mFeatureInfo->hasVulkanAuxCommandMemory) {
+                // for suballocations, deviceMemory is an alias VkDeviceMemory
+                // get underling VkDeviceMemory for given alias
+                deviceMemoryTransform_tohost(&deviceMemory, 1 /*memoryCount*/, &dataOffset,
+                                             1 /*offsetCount*/, nullptr /*size*/, 0 /*sizeCount*/,
+                                             nullptr /*typeIndex*/, 0 /*typeIndexCount*/,
+                                             nullptr /*typeBits*/, 0 /*typeBitCounts*/);
 
+                // mark stream as flushing before flushing commands
+                cmdBufStream->markFlushing();
+                enc->vkQueueFlushCommandsFromAuxMemoryGOOGLE(queue, cmdbuf, deviceMemory,
+                                                             dataOffset, written, true /*do lock*/);
+            } else {
+                enc->vkQueueFlushCommandsGOOGLE(queue, cmdbuf, written, (const void*)writtenPtr,
+                                                true /* do lock */);
+            }
             // Reset this stream.
-            ((CommandBufferStagingStream*)cb->privateStream)->reset();
+            // flushing happens on vkQueueSubmit
+            // vulkan api states that on queue submit,
+            // applications MUST not attempt to modify the command buffer in any way
+            // -as the device may be processing the commands recorded to it.
+            // It is safe to call reset() here for this reason.
+            // Command Buffer associated with this stream will only leave pending state
+            // after queue submit is complete and host has read the data
+            cmdBufStream->reset();
         }
     }
 
@@ -6603,6 +6648,69 @@
         return 0;
     }
 
+    CommandBufferStagingStream::Alloc getAlloc() {
+        if (mFeatureInfo->hasVulkanAuxCommandMemory) {
+            return [this](size_t size) -> CommandBufferStagingStream::Memory {
+                VkMemoryAllocateInfo info{
+                    .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+                    .pNext = nullptr,
+                    .allocationSize = size,
+                    .memoryTypeIndex = VK_MAX_MEMORY_TYPES  // indicates auxiliary memory
+                };
+
+                auto enc = ResourceTracker::getThreadLocalEncoder();
+                VkDevice device = VK_NULL_HANDLE;
+                VkDeviceMemory vkDeviceMem = VK_NULL_HANDLE;
+                VkResult result = getCoherentMemory(&info, enc, device, &vkDeviceMem);
+                if (result != VK_SUCCESS) {
+                    ALOGE("Failed to get coherent memory %u", result);
+                    return {.deviceMemory = VK_NULL_HANDLE, .ptr = nullptr};
+                }
+
+                // getCoherentMemory() uses suballocations.
+                // To retrieve the suballocated memory address, look up
+                // VkDeviceMemory filled in by getCoherentMemory()
+                // scope of mLock
+                {
+                    AutoLock<RecursiveLock> lock(mLock);
+                    const auto it = info_VkDeviceMemory.find(vkDeviceMem);
+                    if (it == info_VkDeviceMemory.end()) {
+                        ALOGE("Coherent memory allocated %u not found", result);
+                        return {.deviceMemory = VK_NULL_HANDLE, .ptr = nullptr};
+                    };
+
+                    const auto& info = it->second;
+                    return {.deviceMemory = vkDeviceMem, .ptr = info.ptr};
+                }
+            };
+        }
+        return nullptr;
+    }
+
+    CommandBufferStagingStream::Free getFree() {
+        if (mFeatureInfo->hasVulkanAuxCommandMemory) {
+            return [this](const CommandBufferStagingStream::Memory& memory) {
+                // deviceMemory may not be the actual backing auxiliary VkDeviceMemory
+                // for suballocations, deviceMemory is a alias VkDeviceMemory hand;
+                // freeCoherentMemoryLocked maps the alias to the backing VkDeviceMemory
+                VkDeviceMemory deviceMemory = memory.deviceMemory;
+                AutoLock<RecursiveLock> lock(mLock);
+                auto it = info_VkDeviceMemory.find(deviceMemory);
+                if (it == info_VkDeviceMemory.end()) {
+                    ALOGE("Device memory to free not found");
+                    return;
+                }
+                auto coherentMemory = freeCoherentMemoryLocked(deviceMemory, it->second);
+                // We have to release the lock before we could possibly free a
+                // CoherentMemory, because that will call into VkEncoder, which
+                // shouldn't be called when the lock is held.
+                lock.unlock();
+                coherentMemory = nullptr;
+            };
+        }
+        return nullptr;
+    }
+
     VkResult on_vkBeginCommandBuffer(
         void* context, VkResult input_result,
         VkCommandBuffer commandBuffer,
@@ -7103,7 +7211,8 @@
     // Resets staging stream for this command buffer and primary command buffers
     // where this command buffer has been recorded. If requested, also clears the pending
     // descriptor sets.
-    void resetCommandBufferStagingInfo(VkCommandBuffer commandBuffer, bool alsoResetPrimaries, bool alsoClearPendingDescriptorSets) {
+    void resetCommandBufferStagingInfo(VkCommandBuffer commandBuffer, bool alsoResetPrimaries,
+                                       bool alsoClearPendingDescriptorSets) {
         struct goldfish_VkCommandBuffer* cb = as_goldfish_VkCommandBuffer(commandBuffer);
         if (!cb) {
             return;
@@ -7299,6 +7408,8 @@
 
     struct goldfish_VkCommandBuffer* cb = as_goldfish_VkCommandBuffer(commandBuffer);
     if (!cb->privateEncoder) {
+        sStaging.setAllocFree(ResourceTracker::get()->getAlloc(),
+                              ResourceTracker::get()->getFree());
         sStaging.popStaging((CommandBufferStagingStream**)&cb->privateStream, &cb->privateEncoder);
     }
     uint8_t* writtenPtr; size_t written;
@@ -8114,6 +8225,9 @@
     return mImpl->syncEncodersForQueue(queue, current);
 }
 
+CommandBufferStagingStream::Alloc ResourceTracker::getAlloc() { return mImpl->getAlloc(); }
+
+CommandBufferStagingStream::Free ResourceTracker::getFree() { return mImpl->getFree(); }
 
 VkResult ResourceTracker::on_vkBeginCommandBuffer(
     void* context, VkResult input_result,
diff --git a/system/vulkan_enc/ResourceTracker.h b/system/vulkan_enc/ResourceTracker.h
index 391eac5..2cb5b2a 100644
--- a/system/vulkan_enc/ResourceTracker.h
+++ b/system/vulkan_enc/ResourceTracker.h
@@ -14,15 +14,15 @@
 // limitations under the License.
 #pragma once
 
-#include "aemu/base/Tracing.h"
-
 #include <vulkan/vulkan.h>
 
-#include "VulkanHandleMapping.h"
-#include "VulkanHandles.h"
 #include <functional>
 #include <memory>
 
+#include "CommandBufferStagingStream.h"
+#include "VulkanHandleMapping.h"
+#include "VulkanHandles.h"
+#include "aemu/base/Tracing.h"
 #include "goldfish_vk_transform_guest.h"
 
 struct EmulatorFeatureInfo;
@@ -528,6 +528,9 @@
     uint32_t syncEncodersForCommandBuffer(VkCommandBuffer commandBuffer, VkEncoder* current);
     uint32_t syncEncodersForQueue(VkQueue queue, VkEncoder* current);
 
+    CommandBufferStagingStream::Alloc getAlloc();
+    CommandBufferStagingStream::Free getFree();
+
     VkResult on_vkBeginCommandBuffer(
         void* context, VkResult input_result,
         VkCommandBuffer commandBuffer,
diff --git a/system/vulkan_enc_unit_tests/Android.mk b/system/vulkan_enc_unit_tests/Android.mk
index 162c95d..e506bc9 100644
--- a/system/vulkan_enc_unit_tests/Android.mk
+++ b/system/vulkan_enc_unit_tests/Android.mk
@@ -6,7 +6,7 @@
 
 LOCAL_C_INCLUDES += \
     device/generic/goldfish-opengl/host/include/libOpenglRender \
-
+    external/gfxstream-protocols/include/vulkan/include/
 
 LOCAL_SRC_FILES:= \
     CommandBufferStagingStream_test.cpp \
diff --git a/system/vulkan_enc_unit_tests/CommandBufferStagingStream_test.cpp b/system/vulkan_enc_unit_tests/CommandBufferStagingStream_test.cpp
index eb34cfb..375bc6e 100644
--- a/system/vulkan_enc_unit_tests/CommandBufferStagingStream_test.cpp
+++ b/system/vulkan_enc_unit_tests/CommandBufferStagingStream_test.cpp
@@ -215,8 +215,9 @@
     // memory source
     std::vector<uint8_t> memorySrc(kTestBufferSize * 2);
 
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
 
     MockAlloc allocFn;
 
@@ -243,8 +244,8 @@
     std::vector<uint8_t> memorySrc(kTestBufferSize * 2);
 
     CommandBufferStagingStream::Memory memory{
-        .deviceMemory = nullptr,  // not needed for this test
-        .ptr = nullptr,           // to test alloc call failing
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = nullptr,                  // to test alloc call failing
     };
 
     MockAlloc allocFn;
@@ -271,8 +272,8 @@
     std::vector<uint8_t> memorySrc(kTestBufferSize * 2);
 
     // device memory for test purposes. The test just needs a pointer
-    uint32_t deviceMem = 0;
-    uint32_t* deviceMemPtr = &deviceMem;
+    uint64_t deviceMem = 0;
+    VkDeviceMemory deviceMemPtr = (VkDeviceMemory)(&deviceMem);
 
     CommandBufferStagingStream::Memory memory{.deviceMemory = deviceMemPtr,
                                               .ptr = memorySrc.data()};
@@ -327,11 +328,12 @@
     // memory source after reallocation
     std::vector<uint8_t> reallocatedMemorySrc(kTestBufferSize * 3);
 
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
 
     CommandBufferStagingStream::Memory reallocatedMemory{
-        .deviceMemory = nullptr,  // not needed for this test
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
         .ptr = reallocatedMemorySrc.data()};
 
     MockAlloc allocFn;
@@ -393,8 +395,9 @@
     // memory source
     std::vector<uint8_t> memorySrc(kTestBufferSize * 2);
 
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
 
     MockAlloc allocFn;
 
@@ -427,8 +430,9 @@
     // memory source
     std::vector<uint8_t> memorySrc(kTestBufferSize * 2);
 
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
 
     MockAlloc allocFn;
 
@@ -463,8 +467,9 @@
     // memory source
     std::vector<uint8_t> memorySrc(kTestBufferSize * 2);
 
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
 
     MockAlloc allocFn;
 
@@ -485,8 +490,9 @@
 TEST(CommandBufferStagingStreamCustomAllocationTest, ReallocationBoundary) {
     // memory source
     std::vector<uint8_t> memorySrc(kTestBufferSize * 3);
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
 
     MockAlloc allocFn;
 
@@ -546,8 +552,9 @@
     // memory source
     std::vector<uint8_t> memorySrc(kTestBufferSize * 3);
 
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
 
     MockAlloc allocFn;
     EXPECT_CALL(allocFn, Call(Ge(kTestBufferSize))).Times(1).WillRepeatedly(Return(memory));
@@ -570,8 +577,9 @@
 TEST(CommandBufferStagingStreamCustomAllocationTest, MetadataCheck) {
     // memory source
     std::vector<uint8_t> memorySrc(kTestBufferSize * 2);
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
 
     // CommandBufferStagingStream allocates metadata when using custom allocators
     static const size_t expectedMetadataSize = 8;
@@ -592,8 +600,9 @@
 TEST(CommandBufferStagingStreamCustomAllocationTest, MarkFlushing) {
     // memory source for  allocation
     std::vector<uint8_t> memorySrc(kTestBufferSize * 2);
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
     MockAlloc allocFn;
 
     EXPECT_CALL(allocFn, Call(Ge(kTestBufferSize))).Times(1).WillRepeatedly(Return(memory));
@@ -622,8 +631,9 @@
     // memory source for  allocation
     // allocate a big enough buffer to avoid resizes in test
     std::vector<uint8_t> memorySrc(kTestBufferSize * 3);
-    CommandBufferStagingStream::Memory memory{.deviceMemory = nullptr,  // not needed for this test
-                                              .ptr = memorySrc.data()};
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
 
     std::condition_variable memoryFlushedCondition;
     std::mutex mutex;
@@ -688,4 +698,27 @@
 
     // wait for read thread to finish
     consumer.join();
+}
+
+// this test verifies that allocBuffer() cannot be called on a stream
+// that is currently being read by the host
+TEST(CommandBufferStagingStreamCustomAllocationTest, AllocBufferFailsIfReadPending) {
+    // memory source for  allocation
+    std::vector<uint8_t> memorySrc(kTestBufferSize * 2);
+    CommandBufferStagingStream::Memory memory{
+        .deviceMemory = VK_NULL_HANDLE,  // not needed for this test
+        .ptr = memorySrc.data()};
+    MockAlloc allocFn;
+
+    EXPECT_CALL(allocFn, Call(Ge(kTestBufferSize))).Times(1).WillRepeatedly(Return(memory));
+
+    auto allocStdFn = allocFn.AsStdFunction();
+    CommandBufferStagingStream stream(allocStdFn, EmptyFree);
+    (void)stream.allocBuffer(kTestBufferSize);
+
+    // will set metadata of the stream buffer to pending read
+    stream.markFlushing();
+
+    EXPECT_DEATH({ (void)stream.allocBuffer(kTestBufferSize); }, "")
+        << "allocBuffer() should not be called while previous data is being flushed";
 }
\ No newline at end of file