Surface: add batch dequeue/queue/cancel methods

Test: atest libgui_test:SurfaceTest
Bug: 113788435

Change-Id: Icd52aeb92e8218b0c201a791ae2a49b79021ac32
diff --git a/libs/gui/Surface.cpp b/libs/gui/Surface.cpp
index 94390aa..101beff 100644
--- a/libs/gui/Surface.cpp
+++ b/libs/gui/Surface.cpp
@@ -618,29 +618,31 @@
     std::mutex mMutex;
 };
 
+void Surface::getDequeueBufferInputLocked(
+        IGraphicBufferProducer::DequeueBufferInput* dequeueInput) {
+    LOG_ALWAYS_FATAL_IF(dequeueInput == nullptr, "input is null");
+
+    dequeueInput->width = mReqWidth ? mReqWidth : mUserWidth;
+    dequeueInput->height = mReqHeight ? mReqHeight : mUserHeight;
+
+    dequeueInput->format = mReqFormat;
+    dequeueInput->usage = mReqUsage;
+
+    dequeueInput->getTimestamps = mEnableFrameTimestamps;
+}
+
 int Surface::dequeueBuffer(android_native_buffer_t** buffer, int* fenceFd) {
     ATRACE_CALL();
     ALOGV("Surface::dequeueBuffer");
 
-    uint32_t reqWidth;
-    uint32_t reqHeight;
-    PixelFormat reqFormat;
-    uint64_t reqUsage;
-    bool enableFrameTimestamps;
-
+    IGraphicBufferProducer::DequeueBufferInput dqInput;
     {
         Mutex::Autolock lock(mMutex);
         if (mReportRemovedBuffers) {
             mRemovedBuffers.clear();
         }
 
-        reqWidth = mReqWidth ? mReqWidth : mUserWidth;
-        reqHeight = mReqHeight ? mReqHeight : mUserHeight;
-
-        reqFormat = mReqFormat;
-        reqUsage = mReqUsage;
-
-        enableFrameTimestamps = mEnableFrameTimestamps;
+        getDequeueBufferInputLocked(&dqInput);
 
         if (mSharedBufferMode && mAutoRefresh && mSharedBufferSlot !=
                 BufferItem::INVALID_BUFFER_SLOT) {
@@ -658,16 +660,17 @@
     nsecs_t startTime = systemTime();
 
     FrameEventHistoryDelta frameTimestamps;
-    status_t result = mGraphicBufferProducer->dequeueBuffer(&buf, &fence, reqWidth, reqHeight,
-                                                            reqFormat, reqUsage, &mBufferAge,
-                                                            enableFrameTimestamps ? &frameTimestamps
-                                                                                  : nullptr);
+    status_t result = mGraphicBufferProducer->dequeueBuffer(&buf, &fence, dqInput.width,
+                                                            dqInput.height, dqInput.format,
+                                                            dqInput.usage, &mBufferAge,
+                                                            dqInput.getTimestamps ?
+                                                                    &frameTimestamps : nullptr);
     mLastDequeueDuration = systemTime() - startTime;
 
     if (result < 0) {
         ALOGV("dequeueBuffer: IGraphicBufferProducer::dequeueBuffer"
                 "(%d, %d, %d, %#" PRIx64 ") failed: %d",
-                reqWidth, reqHeight, reqFormat, reqUsage, result);
+                dqInput.width, dqInput.height, dqInput.format, dqInput.usage, result);
         return result;
     }
 
@@ -696,7 +699,7 @@
         freeAllBuffers();
     }
 
-    if (enableFrameTimestamps) {
+    if (dqInput.getTimestamps) {
          mFrameEventHistory->applyDelta(frameTimestamps);
     }
 
@@ -739,6 +742,176 @@
     return OK;
 }
 
+int Surface::dequeueBuffers(std::vector<BatchBuffer>* buffers) {
+    using DequeueBufferInput = IGraphicBufferProducer::DequeueBufferInput;
+    using DequeueBufferOutput = IGraphicBufferProducer::DequeueBufferOutput;
+    using CancelBufferInput = IGraphicBufferProducer::CancelBufferInput;
+    using RequestBufferOutput = IGraphicBufferProducer::RequestBufferOutput;
+
+    ATRACE_CALL();
+    ALOGV("Surface::dequeueBuffers");
+
+    if (buffers->size() == 0) {
+        ALOGE("%s: must dequeue at least 1 buffer!", __FUNCTION__);
+        return BAD_VALUE;
+    }
+
+    if (mSharedBufferMode) {
+        ALOGE("%s: batch operation is not supported in shared buffer mode!",
+                __FUNCTION__);
+        return INVALID_OPERATION;
+    }
+
+    size_t numBufferRequested = buffers->size();
+    DequeueBufferInput input;
+
+    {
+        Mutex::Autolock lock(mMutex);
+        if (mReportRemovedBuffers) {
+            mRemovedBuffers.clear();
+        }
+
+        getDequeueBufferInputLocked(&input);
+    } // Drop the lock so that we can still touch the Surface while blocking in IGBP::dequeueBuffers
+
+    std::vector<DequeueBufferInput> dequeueInput(numBufferRequested, input);
+    std::vector<DequeueBufferOutput> dequeueOutput;
+
+    nsecs_t startTime = systemTime();
+
+    status_t result = mGraphicBufferProducer->dequeueBuffers(dequeueInput, &dequeueOutput);
+
+    mLastDequeueDuration = systemTime() - startTime;
+
+    if (result < 0) {
+        ALOGV("%s: IGraphicBufferProducer::dequeueBuffers"
+                "(%d, %d, %d, %#" PRIx64 ") failed: %d",
+                __FUNCTION__, input.width, input.height, input.format, input.usage, result);
+        return result;
+    }
+
+    std::vector<CancelBufferInput> cancelBufferInputs(numBufferRequested);
+    std::vector<status_t> cancelBufferOutputs;
+    for (size_t i = 0; i < numBufferRequested; i++) {
+        cancelBufferInputs[i].slot = dequeueOutput[i].slot;
+        cancelBufferInputs[i].fence = dequeueOutput[i].fence;
+    }
+
+    for (const auto& output : dequeueOutput) {
+        if (output.result < 0) {
+            mGraphicBufferProducer->cancelBuffers(cancelBufferInputs, &cancelBufferOutputs);
+            ALOGV("%s: IGraphicBufferProducer::dequeueBuffers"
+                    "(%d, %d, %d, %#" PRIx64 ") failed: %d",
+                    __FUNCTION__, input.width, input.height, input.format, input.usage,
+                    output.result);
+            return output.result;
+        }
+
+        if (output.slot < 0 || output.slot >= NUM_BUFFER_SLOTS) {
+            mGraphicBufferProducer->cancelBuffers(cancelBufferInputs, &cancelBufferOutputs);
+            ALOGE("%s: IGraphicBufferProducer returned invalid slot number %d",
+                    __FUNCTION__, output.slot);
+            android_errorWriteLog(0x534e4554, "36991414"); // SafetyNet logging
+            return FAILED_TRANSACTION;
+        }
+
+        if (input.getTimestamps && !output.timestamps.has_value()) {
+            mGraphicBufferProducer->cancelBuffers(cancelBufferInputs, &cancelBufferOutputs);
+            ALOGE("%s: no frame timestamp returns!", __FUNCTION__);
+            return FAILED_TRANSACTION;
+        }
+
+        // this should never happen
+        ALOGE_IF(output.fence == nullptr,
+                "%s: received null Fence! slot=%d", __FUNCTION__, output.slot);
+    }
+
+    Mutex::Autolock lock(mMutex);
+
+    // Write this while holding the mutex
+    mLastDequeueStartTime = startTime;
+
+    std::vector<int32_t> requestBufferSlots;
+    requestBufferSlots.reserve(numBufferRequested);
+    // handle release all buffers and request buffers
+    for (const auto& output : dequeueOutput) {
+        if (output.result & IGraphicBufferProducer::RELEASE_ALL_BUFFERS) {
+            ALOGV("%s: RELEASE_ALL_BUFFERS during batch operation", __FUNCTION__);
+            freeAllBuffers();
+            break;
+        }
+    }
+
+    for (const auto& output : dequeueOutput) {
+        // Collect slots that needs requesting buffer
+        sp<GraphicBuffer>& gbuf(mSlots[output.slot].buffer);
+        if ((result & IGraphicBufferProducer::BUFFER_NEEDS_REALLOCATION) || gbuf == nullptr) {
+            if (mReportRemovedBuffers && (gbuf != nullptr)) {
+                mRemovedBuffers.push_back(gbuf);
+            }
+            requestBufferSlots.push_back(output.slot);
+        }
+    }
+
+    // Batch request Buffer
+    std::vector<RequestBufferOutput> reqBufferOutput;
+    if (requestBufferSlots.size() > 0) {
+        result = mGraphicBufferProducer->requestBuffers(requestBufferSlots, &reqBufferOutput);
+        if (result != NO_ERROR) {
+            ALOGE("%s: IGraphicBufferProducer::requestBuffers failed: %d",
+                    __FUNCTION__, result);
+            mGraphicBufferProducer->cancelBuffers(cancelBufferInputs, &cancelBufferOutputs);
+            return result;
+        }
+
+        // Check if we have any single failure
+        for (size_t i = 0; i < requestBufferSlots.size(); i++) {
+            if (reqBufferOutput[i].result != OK) {
+                ALOGE("%s: IGraphicBufferProducer::requestBuffers failed at %zu-th buffer, slot %d",
+                        __FUNCTION__, i, requestBufferSlots[i]);
+                mGraphicBufferProducer->cancelBuffers(cancelBufferInputs, &cancelBufferOutputs);
+                return reqBufferOutput[i].result;
+            }
+        }
+
+        // Fill request buffer results to mSlots
+        for (size_t i = 0; i < requestBufferSlots.size(); i++) {
+            mSlots[requestBufferSlots[i]].buffer = reqBufferOutput[i].buffer;
+        }
+    }
+
+    for (size_t batchIdx = 0; batchIdx < numBufferRequested; batchIdx++) {
+        const auto& output = dequeueOutput[batchIdx];
+        int slot = output.slot;
+        sp<GraphicBuffer>& gbuf(mSlots[slot].buffer);
+
+        if (CC_UNLIKELY(atrace_is_tag_enabled(ATRACE_TAG_GRAPHICS))) {
+            static FenceMonitor hwcReleaseThread("HWC release");
+            hwcReleaseThread.queueFence(output.fence);
+        }
+
+        if (input.getTimestamps) {
+             mFrameEventHistory->applyDelta(output.timestamps.value());
+        }
+
+        if (output.fence->isValid()) {
+            buffers->at(batchIdx).fenceFd = output.fence->dup();
+            if (buffers->at(batchIdx).fenceFd == -1) {
+                ALOGE("%s: error duping fence: %d", __FUNCTION__, errno);
+                // dup() should never fail; something is badly wrong. Soldier on
+                // and hope for the best; the worst that should happen is some
+                // visible corruption that lasts until the next frame.
+            }
+        } else {
+            buffers->at(batchIdx).fenceFd = -1;
+        }
+
+        buffers->at(batchIdx).buffer = gbuf.get();
+        mDequeuedSlots.insert(slot);
+    }
+    return OK;
+}
+
 int Surface::cancelBuffer(android_native_buffer_t* buffer,
         int fenceFd) {
     ATRACE_CALL();
@@ -769,15 +942,65 @@
     return OK;
 }
 
+int Surface::cancelBuffers(const std::vector<BatchBuffer>& buffers) {
+    using CancelBufferInput = IGraphicBufferProducer::CancelBufferInput;
+    ATRACE_CALL();
+    ALOGV("Surface::cancelBuffers");
+
+    if (mSharedBufferMode) {
+        ALOGE("%s: batch operation is not supported in shared buffer mode!",
+                __FUNCTION__);
+        return INVALID_OPERATION;
+    }
+
+    size_t numBuffers = buffers.size();
+    std::vector<CancelBufferInput> cancelBufferInputs(numBuffers);
+    std::vector<status_t> cancelBufferOutputs;
+    size_t numBuffersCancelled = 0;
+    int badSlotResult = 0;
+    for (size_t i = 0; i < numBuffers; i++) {
+        int slot = getSlotFromBufferLocked(buffers[i].buffer);
+        int fenceFd = buffers[i].fenceFd;
+        if (slot < 0) {
+            if (fenceFd >= 0) {
+                close(fenceFd);
+            }
+            ALOGE("%s: cannot find slot number for cancelled buffer", __FUNCTION__);
+            badSlotResult = slot;
+        } else {
+            sp<Fence> fence(fenceFd >= 0 ? new Fence(fenceFd) : Fence::NO_FENCE);
+            cancelBufferInputs[numBuffersCancelled].slot = slot;
+            cancelBufferInputs[numBuffersCancelled++].fence = fence;
+        }
+    }
+    cancelBufferInputs.resize(numBuffersCancelled);
+    mGraphicBufferProducer->cancelBuffers(cancelBufferInputs, &cancelBufferOutputs);
+
+
+    for (size_t i = 0; i < numBuffersCancelled; i++) {
+        mDequeuedSlots.erase(cancelBufferInputs[i].slot);
+    }
+
+    if (badSlotResult != 0) {
+        return badSlotResult;
+    }
+    return OK;
+}
+
 int Surface::getSlotFromBufferLocked(
         android_native_buffer_t* buffer) const {
+    if (buffer == nullptr) {
+        ALOGE("%s: input buffer is null!", __FUNCTION__);
+        return BAD_VALUE;
+    }
+
     for (int i = 0; i < NUM_BUFFER_SLOTS; i++) {
         if (mSlots[i].buffer != nullptr &&
                 mSlots[i].buffer->handle == buffer->handle) {
             return i;
         }
     }
-    ALOGE("getSlotFromBufferLocked: unknown buffer: %p", buffer->handle);
+    ALOGE("%s: unknown buffer: %p", __FUNCTION__, buffer->handle);
     return BAD_VALUE;
 }
 
@@ -787,42 +1010,22 @@
     return OK;
 }
 
-int Surface::queueBuffer(android_native_buffer_t* buffer, int fenceFd) {
-    ATRACE_CALL();
-    ALOGV("Surface::queueBuffer");
-    Mutex::Autolock lock(mMutex);
-    int64_t timestamp;
+void Surface::getQueueBufferInputLocked(android_native_buffer_t* buffer, int fenceFd,
+        nsecs_t timestamp, IGraphicBufferProducer::QueueBufferInput* out) {
     bool isAutoTimestamp = false;
 
-    if (mTimestamp == NATIVE_WINDOW_TIMESTAMP_AUTO) {
+    if (timestamp == NATIVE_WINDOW_TIMESTAMP_AUTO) {
         timestamp = systemTime(SYSTEM_TIME_MONOTONIC);
         isAutoTimestamp = true;
         ALOGV("Surface::queueBuffer making up timestamp: %.2f ms",
             timestamp / 1000000.0);
-    } else {
-        timestamp = mTimestamp;
     }
-    int i = getSlotFromBufferLocked(buffer);
-    if (i < 0) {
-        if (fenceFd >= 0) {
-            close(fenceFd);
-        }
-        return i;
-    }
-    if (mSharedBufferSlot == i && mSharedBufferHasBeenQueued) {
-        if (fenceFd >= 0) {
-            close(fenceFd);
-        }
-        return OK;
-    }
-
 
     // Make sure the crop rectangle is entirely inside the buffer.
     Rect crop(Rect::EMPTY_RECT);
     mCrop.intersect(Rect(buffer->width, buffer->height), &crop);
 
     sp<Fence> fence(fenceFd >= 0 ? new Fence(fenceFd) : Fence::NO_FENCE);
-    IGraphicBufferProducer::QueueBufferOutput output;
     IGraphicBufferProducer::QueueBufferInput input(timestamp, isAutoTimestamp,
             static_cast<android_dataspace>(mDataSpace), crop, mScalingMode,
             mTransform ^ mStickyTransform, fence, mStickyTransform,
@@ -893,15 +1096,12 @@
 
         input.setSurfaceDamage(flippedRegion);
     }
+    *out = input;
+}
 
-    nsecs_t now = systemTime();
-    status_t err = mGraphicBufferProducer->queueBuffer(i, input, &output);
-    mLastQueueDuration = systemTime() - now;
-    if (err != OK)  {
-        ALOGE("queueBuffer: error queuing buffer to SurfaceTexture, %d", err);
-    }
-
-    mDequeuedSlots.erase(i);
+void Surface::onBufferQueuedLocked(int slot, sp<Fence> fence,
+        const IGraphicBufferProducer::QueueBufferOutput& output) {
+    mDequeuedSlots.erase(slot);
 
     if (mEnableFrameTimestamps) {
         mFrameEventHistory->applyDelta(output.frameTimestamps);
@@ -935,7 +1135,7 @@
         mDirtyRegion = Region::INVALID_REGION;
     }
 
-    if (mSharedBufferMode && mAutoRefresh && mSharedBufferSlot == i) {
+    if (mSharedBufferMode && mAutoRefresh && mSharedBufferSlot == slot) {
         mSharedBufferHasBeenQueued = true;
     }
 
@@ -945,6 +1145,89 @@
         static FenceMonitor gpuCompletionThread("GPU completion");
         gpuCompletionThread.queueFence(fence);
     }
+}
+
+int Surface::queueBuffer(android_native_buffer_t* buffer, int fenceFd) {
+    ATRACE_CALL();
+    ALOGV("Surface::queueBuffer");
+    Mutex::Autolock lock(mMutex);
+
+    int i = getSlotFromBufferLocked(buffer);
+    if (i < 0) {
+        if (fenceFd >= 0) {
+            close(fenceFd);
+        }
+        return i;
+    }
+    if (mSharedBufferSlot == i && mSharedBufferHasBeenQueued) {
+        if (fenceFd >= 0) {
+            close(fenceFd);
+        }
+        return OK;
+    }
+
+    IGraphicBufferProducer::QueueBufferOutput output;
+    IGraphicBufferProducer::QueueBufferInput input;
+    getQueueBufferInputLocked(buffer, fenceFd, mTimestamp, &input);
+    sp<Fence> fence = input.fence;
+
+    nsecs_t now = systemTime();
+    status_t err = mGraphicBufferProducer->queueBuffer(i, input, &output);
+    mLastQueueDuration = systemTime() - now;
+    if (err != OK)  {
+        ALOGE("queueBuffer: error queuing buffer, %d", err);
+    }
+
+    onBufferQueuedLocked(i, fence, output);
+    return err;
+}
+
+int Surface::queueBuffers(const std::vector<BatchQueuedBuffer>& buffers) {
+    ATRACE_CALL();
+    ALOGV("Surface::queueBuffers");
+    Mutex::Autolock lock(mMutex);
+
+    if (mSharedBufferMode) {
+        ALOGE("%s: batched operation is not supported in shared buffer mode", __FUNCTION__);
+        return INVALID_OPERATION;
+    }
+
+    size_t numBuffers = buffers.size();
+    std::vector<IGraphicBufferProducer::QueueBufferInput> queueBufferInputs(numBuffers);
+    std::vector<IGraphicBufferProducer::QueueBufferOutput> queueBufferOutputs;
+    std::vector<int> bufferSlots(numBuffers, -1);
+    std::vector<sp<Fence>> bufferFences(numBuffers);
+
+    for (size_t batchIdx = 0; batchIdx < numBuffers; batchIdx++) {
+        int i = getSlotFromBufferLocked(buffers[batchIdx].buffer);
+        if (i < 0) {
+            if (buffers[batchIdx].fenceFd >= 0) {
+                close(buffers[batchIdx].fenceFd);
+            }
+            return i;
+        }
+        bufferSlots[batchIdx] = i;
+
+        IGraphicBufferProducer::QueueBufferInput input;
+        getQueueBufferInputLocked(
+                buffers[batchIdx].buffer, buffers[batchIdx].fenceFd, buffers[batchIdx].timestamp,
+                &input);
+        bufferFences[batchIdx] = input.fence;
+        queueBufferInputs[batchIdx] = input;
+    }
+
+    nsecs_t now = systemTime();
+    status_t err = mGraphicBufferProducer->queueBuffers(queueBufferInputs, &queueBufferOutputs);
+    mLastQueueDuration = systemTime() - now;
+    if (err != OK)  {
+        ALOGE("%s: error queuing buffer, %d", __FUNCTION__, err);
+    }
+
+
+    for (size_t batchIdx = 0; batchIdx < numBuffers; batchIdx++) {
+        onBufferQueuedLocked(bufferSlots[batchIdx], bufferFences[batchIdx],
+                queueBufferOutputs[batchIdx]);
+    }
 
     return err;
 }
diff --git a/libs/gui/include/gui/Surface.h b/libs/gui/include/gui/Surface.h
index 82bc5c9..43b5dcd 100644
--- a/libs/gui/include/gui/Surface.h
+++ b/libs/gui/include/gui/Surface.h
@@ -344,6 +344,23 @@
     static status_t attachAndQueueBufferWithDataspace(Surface* surface, sp<GraphicBuffer> buffer,
                                                       ui::Dataspace dataspace);
 
+    // Batch version of dequeueBuffer, cancelBuffer and queueBuffer
+    // Note that these batched operations are not supported when shared buffer mode is being used.
+    struct BatchBuffer {
+        ANativeWindowBuffer* buffer = nullptr;
+        int fenceFd = -1;
+    };
+    virtual int dequeueBuffers(std::vector<BatchBuffer>* buffers);
+    virtual int cancelBuffers(const std::vector<BatchBuffer>& buffers);
+
+    struct BatchQueuedBuffer {
+        ANativeWindowBuffer* buffer = nullptr;
+        int fenceFd = -1;
+        nsecs_t timestamp = NATIVE_WINDOW_TIMESTAMP_AUTO;
+    };
+    virtual int queueBuffers(
+            const std::vector<BatchQueuedBuffer>& buffers);
+
 protected:
     enum { NUM_BUFFER_SLOTS = BufferQueueDefs::NUM_BUFFER_SLOTS };
     enum { DEFAULT_FORMAT = PIXEL_FORMAT_RGBA_8888 };
@@ -373,6 +390,14 @@
     void freeAllBuffers();
     int getSlotFromBufferLocked(android_native_buffer_t* buffer) const;
 
+    void getDequeueBufferInputLocked(IGraphicBufferProducer::DequeueBufferInput* dequeueInput);
+
+    void getQueueBufferInputLocked(android_native_buffer_t* buffer, int fenceFd, nsecs_t timestamp,
+            IGraphicBufferProducer::QueueBufferInput* out);
+
+    void onBufferQueuedLocked(int slot, sp<Fence> fence,
+            const IGraphicBufferProducer::QueueBufferOutput& output);
+
     struct BufferSlot {
         sp<GraphicBuffer> buffer;
         Region dirtyRegion;
diff --git a/libs/gui/tests/Surface_test.cpp b/libs/gui/tests/Surface_test.cpp
index 7761db8..60a32f0 100644
--- a/libs/gui/tests/Surface_test.cpp
+++ b/libs/gui/tests/Surface_test.cpp
@@ -2024,4 +2024,86 @@
     EXPECT_EQ(BufferQueueDefs::NUM_BUFFER_SLOTS, count);
 }
 
+TEST_F(SurfaceTest, BatchOperations) {
+    const int BUFFER_COUNT = 16;
+    const int BATCH_SIZE = 8;
+    sp<IGraphicBufferProducer> producer;
+    sp<IGraphicBufferConsumer> consumer;
+    BufferQueue::createBufferQueue(&producer, &consumer);
+
+    sp<CpuConsumer> cpuConsumer = new CpuConsumer(consumer, 1);
+    sp<Surface> surface = new Surface(producer);
+    sp<ANativeWindow> window(surface);
+    sp<StubProducerListener> listener = new StubProducerListener();
+
+    ASSERT_EQ(OK, surface->connect(NATIVE_WINDOW_API_CPU, /*listener*/listener,
+            /*reportBufferRemoval*/false));
+
+    ASSERT_EQ(NO_ERROR, native_window_set_buffer_count(window.get(), BUFFER_COUNT));
+
+    std::vector<Surface::BatchBuffer> buffers(BATCH_SIZE);
+
+    // Batch dequeued buffers can be queued individually
+    ASSERT_EQ(NO_ERROR, surface->dequeueBuffers(&buffers));
+    for (size_t i = 0; i < BATCH_SIZE; i++) {
+        ANativeWindowBuffer* buffer = buffers[i].buffer;
+        int fence = buffers[i].fenceFd;
+        ASSERT_EQ(NO_ERROR, window->queueBuffer(window.get(), buffer, fence));
+    }
+
+    // Batch dequeued buffers can be canceled individually
+    ASSERT_EQ(NO_ERROR, surface->dequeueBuffers(&buffers));
+    for (size_t i = 0; i < BATCH_SIZE; i++) {
+        ANativeWindowBuffer* buffer = buffers[i].buffer;
+        int fence = buffers[i].fenceFd;
+        ASSERT_EQ(NO_ERROR, window->cancelBuffer(window.get(), buffer, fence));
+    }
+
+    // Batch dequeued buffers can be batch cancelled
+    ASSERT_EQ(NO_ERROR, surface->dequeueBuffers(&buffers));
+    ASSERT_EQ(NO_ERROR, surface->cancelBuffers(buffers));
+
+    // Batch dequeued buffers can be batch queued
+    ASSERT_EQ(NO_ERROR, surface->dequeueBuffers(&buffers));
+    std::vector<Surface::BatchQueuedBuffer> queuedBuffers(BATCH_SIZE);
+    for (size_t i = 0; i < BATCH_SIZE; i++) {
+        queuedBuffers[i].buffer = buffers[i].buffer;
+        queuedBuffers[i].fenceFd = buffers[i].fenceFd;
+        queuedBuffers[i].timestamp = NATIVE_WINDOW_TIMESTAMP_AUTO;
+    }
+    ASSERT_EQ(NO_ERROR, surface->queueBuffers(queuedBuffers));
+
+    ASSERT_EQ(NO_ERROR, surface->disconnect(NATIVE_WINDOW_API_CPU));
+}
+
+TEST_F(SurfaceTest, BatchIllegalOperations) {
+    const int BUFFER_COUNT = 16;
+    const int BATCH_SIZE = 8;
+    sp<IGraphicBufferProducer> producer;
+    sp<IGraphicBufferConsumer> consumer;
+    BufferQueue::createBufferQueue(&producer, &consumer);
+
+    sp<CpuConsumer> cpuConsumer = new CpuConsumer(consumer, 1);
+    sp<Surface> surface = new Surface(producer);
+    sp<ANativeWindow> window(surface);
+    sp<StubProducerListener> listener = new StubProducerListener();
+
+    ASSERT_EQ(OK, surface->connect(NATIVE_WINDOW_API_CPU, /*listener*/listener,
+            /*reportBufferRemoval*/false));
+
+    ASSERT_EQ(NO_ERROR, native_window_set_buffer_count(window.get(), BUFFER_COUNT));
+
+    std::vector<Surface::BatchBuffer> buffers(BATCH_SIZE);
+    std::vector<Surface::BatchQueuedBuffer> queuedBuffers(BATCH_SIZE);
+
+    // Batch operations are invalid in shared buffer mode
+    surface->setSharedBufferMode(true);
+    ASSERT_EQ(INVALID_OPERATION, surface->dequeueBuffers(&buffers));
+    ASSERT_EQ(INVALID_OPERATION, surface->cancelBuffers(buffers));
+    ASSERT_EQ(INVALID_OPERATION, surface->queueBuffers(queuedBuffers));
+    surface->setSharedBufferMode(false);
+
+    ASSERT_EQ(NO_ERROR, surface->disconnect(NATIVE_WINDOW_API_CPU));
+}
+
 } // namespace android