// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/viz/service/display/display_resource_provider_skia.h"

#include <stddef.h>
#include <stdint.h>

#include <memory>
#include <optional>
#include <set>
#include <unordered_map>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_refptr.h"
#include "build/build_config.h"
#include "components/viz/client/client_resource_provider.h"
#include "components/viz/common/resources/release_callback.h"
#include "components/viz/common/resources/returned_resource.h"
#include "components/viz/common/resources/transferable_resource.h"
#include "components/viz/test/test_context_provider.h"
#include "gpu/command_buffer/client/client_shared_image.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/khronos/GLES2/gl2.h"
#include "third_party/khronos/GLES2/gl2ext.h"
#include "third_party/skia/include/core/SkColorSpace.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/gpu_fence_handle.h"

using testing::_;
using testing::ByMove;
using testing::DoAll;
using testing::Return;
using testing::SaveArg;

namespace viz {
namespace {

class MockReleaseCallback {
 public:
  MOCK_METHOD2(Released, void(const gpu::SyncToken& token, bool lost));
};

MATCHER_P(SamePtr, ptr_to_expected, "") {
  return arg.get() == ptr_to_expected;
}

static void CollectResources(std::vector<ReturnedResource>* array,
                             std::vector<ReturnedResourceViz> returned) {
  for (auto& resource_viz : returned) {
    ReturnedResource resource{
        resource_viz.id,
        gpu::SharedImageExportResult::CreateForTesting(resource_viz.sync_token),
        std::move(resource_viz.release_fence), resource_viz.count,
        resource_viz.lost};
    array->emplace_back(std::move(resource));
  }
}

class MockExternalUseClient : public ExternalUseClient {
 public:
  MockExternalUseClient() = default;
  MOCK_METHOD1(ReleaseImageContexts,
               gpu::SyncToken(
                   std::vector<std::unique_ptr<ImageContext>> image_contexts));
  MOCK_METHOD4(
      CreateImageContext,
      std::unique_ptr<ImageContext>(const TransferableResource& resource,
                                    bool,
                                    bool,
                                    uint32_t));
};

class DisplayResourceProviderSkiaTest : public testing::Test {
 public:
  DisplayResourceProviderSkiaTest() {
    child_context_provider_ = TestContextProvider::CreateGLES();
    child_context_provider_->BindToCurrentSequence();
    child_resource_provider_ = std::make_unique<ClientResourceProvider>();
  }

  ~DisplayResourceProviderSkiaTest() override {
    child_resource_provider_->ShutdownAndReleaseAllResources();
  }

  void SetUp() override {
    resource_provider_ = std::make_unique<DisplayResourceProviderSkia>();
    lock_set_.emplace(resource_provider_.get(), &client_);
  }

  void TearDown() override {
    resource_provider_.reset();
    lock_set_.reset();
  }

  static ReturnCallback GetReturnCallback(
      std::vector<ReturnedResource>* array) {
    return base::BindRepeating(&CollectResources, array);
  }

  TransferableResource CreateResource() {
    gpu::SyncToken sync_token = GenSyncToken();
    EXPECT_TRUE(sync_token.HasData());

    return TransferableResource::Make(
        gpu::ClientSharedImage::CreateForTesting(),
        TransferableResource::ResourceSource::kTest, sync_token);
  }

  gpu::SyncToken GenSyncToken() {
    gpu::SyncToken sync_token(gpu::CommandBufferNamespace::GPU_IO,
                              gpu::CommandBufferId::FromUnsafeValue(0x123),
                              next_fence_sync_++);
    sync_token.SetVerifyFlush();
    return sync_token;
  }

 protected:
  uint64_t next_fence_sync_ = 1;
  scoped_refptr<TestContextProvider> child_context_provider_;
  std::unique_ptr<DisplayResourceProviderSkia> resource_provider_;
  std::unique_ptr<ClientResourceProvider> child_resource_provider_;
  testing::NiceMock<MockExternalUseClient> client_;
  std::optional<DisplayResourceProviderSkia::LockSetForExternalUse> lock_set_;
};

TEST_F(DisplayResourceProviderSkiaTest, LockForExternalUse) {
  auto gl_resource = CreateResource();
  ResourceId id1 =
      child_resource_provider_->ImportResource(gl_resource, base::DoNothing());
  std::vector<ReturnedResource> returned_to_child;
  int child_id = resource_provider_->CreateChild(
      GetReturnCallback(&returned_to_child), SurfaceId());

  // Transfer some resources to the parent.
  std::vector<TransferableResource> list;

  CHECK(child_context_provider_);
  child_resource_provider_->PrepareSendToParent(
      {id1}, &list, child_context_provider_->SharedImageInterface());
  ASSERT_EQ(1u, list.size());
  EXPECT_TRUE(child_resource_provider_->InUseByConsumer(id1));

  resource_provider_->ReceiveFromChild(child_id, list);

  // In DisplayResourceProvider's namespace, use the mapped resource id.
  std::unordered_map<ResourceId, ResourceId, ResourceIdHasher> resource_map =
      resource_provider_->GetChildToParentMap(child_id);

  ResourceId parent_id = resource_map[list.front().id];

  EXPECT_CALL(client_, CreateImageContext(_, _, _, _))
      .WillOnce([&](const TransferableResource& resource,
                    bool maybe_concurrent_reads, bool raw_draw_if_possible,
                    uint32_t client_id) {
        return std::make_unique<ExternalUseClient::ImageContext>(resource);
      });

  ExternalUseClient::ImageContext* locked_image_context =
      lock_set_->LockResource(parent_id, /*maybe_concurrent_reads=*/true,
                              /*is_video_plane=*/false);
  ASSERT_EQ(locked_image_context->mailbox(), gl_resource.mailbox());
  ASSERT_EQ(locked_image_context->sync_token(), gl_resource.sync_token());

  // Don't release while locked.
  EXPECT_CALL(client_, ReleaseImageContexts(_)).Times(0);
  // Return the resources back to the child. Nothing should happen because
  // of the resource lock.
  resource_provider_->DeclareUsedResourcesFromChild(child_id, ResourceIdSet());
  // The resource should not be returned due to the external use lock.
  EXPECT_EQ(0u, returned_to_child.size());

  gpu::SyncToken sync_token2 = GenSyncToken();
  gpu::SyncToken sync_token3 = GenSyncToken();
  // We will get a second release of |parent_id| now that we've released our
  // external lock.
  EXPECT_CALL(client_, ReleaseImageContexts(
                           testing::ElementsAre(SamePtr(locked_image_context))))
      .WillOnce(Return(sync_token3));
  // UnlockResources will also call DeclareUsedResourcesFromChild.
  lock_set_->UnlockResources(sync_token2);
  // The resource should be returned after the lock is released.
  EXPECT_EQ(1u, returned_to_child.size());
  EXPECT_TRUE(returned_to_child[0].shared_image_export_result.IsEqualForTesting(
      sync_token3));
  child_resource_provider_->ReceiveReturnsFromParent(
      std::move(returned_to_child));
  child_resource_provider_->RemoveImportedResource(id1);
}

TEST_F(DisplayResourceProviderSkiaTest, LockForExternalUseWebView) {
  auto gl_resource = CreateResource();
  ResourceId id1 =
      child_resource_provider_->ImportResource(gl_resource, base::DoNothing());
  std::vector<ReturnedResource> returned_to_child;
  int child_id = resource_provider_->CreateChild(
      GetReturnCallback(&returned_to_child), SurfaceId());

  // Transfer some resources to the parent.
  std::vector<TransferableResource> list;

  CHECK(child_context_provider_);
  child_resource_provider_->PrepareSendToParent(
      {id1}, &list, child_context_provider_->SharedImageInterface());
  ASSERT_EQ(1u, list.size());
  EXPECT_TRUE(child_resource_provider_->InUseByConsumer(id1));

  resource_provider_->ReceiveFromChild(child_id, list);

  // In DisplayResourceProvider's namespace, use the mapped resource id.
  std::unordered_map<ResourceId, ResourceId, ResourceIdHasher> resource_map =
      resource_provider_->GetChildToParentMap(child_id);

  ResourceId parent_id = resource_map[list.front().id];

  EXPECT_CALL(client_, CreateImageContext(_, _, _, _))
      .WillOnce([&](const TransferableResource& resource,
                    bool maybe_concurrent_reads, bool raw_draw_if_possible,
                    uint32_t client_id) {
        return std::make_unique<ExternalUseClient::ImageContext>(resource);
      });

  ExternalUseClient::ImageContext* locked_image_context =
      lock_set_->LockResource(parent_id, /*maybe_concurrent_reads=*/true,
                              /*is_video_plane=*/false);
  ASSERT_EQ(gl_resource.mailbox(), locked_image_context->mailbox());
  ASSERT_EQ(gl_resource.sync_token(), locked_image_context->sync_token());

  // Don't release while locked.
  EXPECT_CALL(client_, ReleaseImageContexts(_)).Times(0);
  // Return the resources back to the child. Nothing should happen because
  // of the resource lock.
  resource_provider_->DeclareUsedResourcesFromChild(child_id, ResourceIdSet());
  // The resource should not be returned due to the external use lock.
  EXPECT_EQ(0u, returned_to_child.size());

  // Disable access to gpu thread.
  resource_provider_->SetAllowAccessToGPUThread(false);

  gpu::SyncToken sync_token2 = GenSyncToken();
  gpu::SyncToken sync_token3 = GenSyncToken();

  // Without GPU thread access no ReleaseImageContexts() should happen
  EXPECT_CALL(client_, ReleaseImageContexts(_)).Times(0);
  // Unlock resources
  lock_set_->UnlockResources(sync_token2);
  // Resources should not be returned because we can't unlock them on GPU
  // thread.
  EXPECT_EQ(0u, returned_to_child.size());

  // We will get a second release of |parent_id| now that we've released our
  // external lock and have access to GPU thread.
  EXPECT_CALL(client_, ReleaseImageContexts(
                           testing::ElementsAre(SamePtr(locked_image_context))))
      .WillOnce(Return(sync_token3));
  // Enable access to GPU Thread
  resource_provider_->SetAllowAccessToGPUThread(true);

  // The resource should be returned after the lock is released.
  EXPECT_EQ(1u, returned_to_child.size());
  EXPECT_TRUE(returned_to_child[0].shared_image_export_result.IsEqualForTesting(
      sync_token3));
  child_resource_provider_->ReceiveReturnsFromParent(
      std::move(returned_to_child));
  child_resource_provider_->RemoveImportedResource(id1);
}

class TestGpuCommandsCompletedFence : public ResourceFence {
 public:
  explicit TestGpuCommandsCompletedFence(
      DisplayResourceProviderSkia* resource_provider)
      : ResourceFence(resource_provider) {}

  // ResourceFence implementation.
  bool HasPassed() override { return passed_; }
  gfx::GpuFenceHandle GetGpuFenceHandle() override { NOTREACHED(); }

  void Signal() {
    passed_ = true;
    FencePassed();
  }

 private:
  ~TestGpuCommandsCompletedFence() override = default;

  bool passed_ = false;
  base::WeakPtr<DisplayResourceProvider> resource_provider_;
};

class TestReleaseFence : public ResourceFence {
 public:
  explicit TestReleaseFence(DisplayResourceProviderSkia* resource_provider)
      : ResourceFence(resource_provider) {}

  // ResourceFence implementation.
  bool HasPassed() override { return release_fence_.has_value(); }
  gfx::GpuFenceHandle GetGpuFenceHandle() override {
    return HasPassed() ? release_fence_->Clone() : gfx::GpuFenceHandle();
  }

  void SetReleaseFence(gfx::GpuFenceHandle release_fence) {
    release_fence_ = std::move(release_fence);
    FencePassed();
  }

 private:
  ~TestReleaseFence() override = default;

  std::optional<gfx::GpuFenceHandle> release_fence_;
  base::WeakPtr<DisplayResourceProvider> resource_provider_;
};

TEST_F(DisplayResourceProviderSkiaTest,
       ResourceFenceStopsReturnToChildOrDelete) {
  const std::vector<TransferableResource::SynchronizationType>
      kSynchronizationTypes = {
          TransferableResource::SynchronizationType::kGpuCommandsCompleted,
          TransferableResource::SynchronizationType::kReleaseFence};
  for (auto sync_type : kSynchronizationTypes) {
    MockReleaseCallback release;

    TransferableResource tran1 = CreateResource();
    tran1.synchronization_type = sync_type;
    ResourceId id1 = child_resource_provider_->ImportResource(
        tran1, base::BindOnce(&MockReleaseCallback::Released,
                              base::Unretained(&release)));

    TransferableResource tran2 = CreateResource();
    ASSERT_EQ(tran2.synchronization_type,
              TransferableResource::SynchronizationType::kSyncToken);
    ResourceId id2 = child_resource_provider_->ImportResource(
        tran2, base::BindOnce(&MockReleaseCallback::Released,
                              base::Unretained(&release)));

    std::vector<ReturnedResource> returned_to_child;
    int child_id = resource_provider_->CreateChild(
        GetReturnCallback(&returned_to_child), SurfaceId());

    // Transfer resources to the parent.
    std::vector<TransferableResource> list;

    CHECK(child_context_provider_);
    child_resource_provider_->PrepareSendToParent(
        {id1, id2}, &list, child_context_provider_->SharedImageInterface());
    ASSERT_EQ(2u, list.size());
    EXPECT_TRUE(child_resource_provider_->InUseByConsumer(id1));
    EXPECT_TRUE(child_resource_provider_->InUseByConsumer(id2));

    resource_provider_->ReceiveFromChild(child_id, list);

    // In DisplayResourceProvider's namespace, use the mapped resource id.
    std::unordered_map<ResourceId, ResourceId, ResourceIdHasher> resource_map =
        resource_provider_->GetChildToParentMap(child_id);

    scoped_refptr<ResourceFence> fence;
    TestGpuCommandsCompletedFence* gpu_commands_completed_fence = nullptr;
    TestReleaseFence* release_fence = nullptr;
    if (sync_type ==
        TransferableResource::SynchronizationType::kGpuCommandsCompleted) {
      fence = base::MakeRefCounted<TestGpuCommandsCompletedFence>(
          resource_provider_.get());
      gpu_commands_completed_fence =
          static_cast<TestGpuCommandsCompletedFence*>(fence.get());
      resource_provider_->SetGpuCommandsCompletedFence(fence.get());
    } else {
      ASSERT_EQ(TransferableResource::SynchronizationType::kReleaseFence,
                sync_type);
      fence = base::MakeRefCounted<TestReleaseFence>(resource_provider_.get());
      release_fence = static_cast<TestReleaseFence*>(fence.get());
      resource_provider_->SetReleaseFence(fence.get());
    }

    {
      for (auto& resource : list) {
        ResourceId parent_id = resource_map[resource.id];
        lock_set_->LockResource(parent_id, /*maybe_concurrent_reads=*/true,
                                /*is_video_plane=*/false);
      }
      lock_set_->UnlockResources(GenSyncToken());
    }
    EXPECT_EQ(0u, returned_to_child.size());
    EXPECT_EQ(2u, resource_provider_->num_resources());

    resource_provider_->DeclareUsedResourcesFromChild(child_id,
                                                      ResourceIdSet());

    EXPECT_EQ(1u, resource_provider_->num_resources());
    EXPECT_EQ(1u, returned_to_child.size());

    // Signalling the resource fence should return the resources automatically.
    if (gpu_commands_completed_fence) {
      gpu_commands_completed_fence->Signal();
    } else {
      gfx::GpuFenceHandle fake_handle;
#if BUILDFLAG(IS_POSIX)
      const int32_t kFenceFd = dup(1);
      fake_handle.Adopt(base::ScopedFD(kFenceFd));
#endif
      release_fence->SetReleaseFence(std::move(fake_handle));
    }

    EXPECT_EQ(0u, resource_provider_->num_resources());
    EXPECT_EQ(2u, returned_to_child.size());

#if BUILDFLAG(IS_POSIX)
    EXPECT_TRUE(returned_to_child[0].release_fence.is_null());
    if (release_fence)
      EXPECT_FALSE(returned_to_child[1].release_fence.is_null());
    else
      EXPECT_TRUE(returned_to_child[1].release_fence.is_null());
#endif

    child_resource_provider_->ReceiveReturnsFromParent(
        std::move(returned_to_child));
    EXPECT_CALL(release, Released(_, _)).Times(2);
    child_resource_provider_->RemoveImportedResource(id1);
    child_resource_provider_->RemoveImportedResource(id2);
  }
}

TEST_F(DisplayResourceProviderSkiaTest, ResourceFenceDestroyChild) {
  const std::vector<TransferableResource::SynchronizationType>
      kSynchronizationTypes = {
          TransferableResource::SynchronizationType::kGpuCommandsCompleted,
          TransferableResource::SynchronizationType::kReleaseFence};
  for (auto sync_type : kSynchronizationTypes) {
    MockReleaseCallback release;

    TransferableResource tran1 = CreateResource();
    tran1.synchronization_type = sync_type;
    ResourceId id1 = child_resource_provider_->ImportResource(
        tran1, base::BindOnce(&MockReleaseCallback::Released,
                              base::Unretained(&release)));

    TransferableResource tran2 = CreateResource();
    ASSERT_EQ(tran2.synchronization_type,
              TransferableResource::SynchronizationType::kSyncToken);
    ResourceId id2 = child_resource_provider_->ImportResource(
        tran2, base::BindOnce(&MockReleaseCallback::Released,
                              base::Unretained(&release)));

    std::vector<ReturnedResource> returned_to_child;
    int child_id = resource_provider_->CreateChild(
        GetReturnCallback(&returned_to_child), SurfaceId());

    // Transfer resources to the parent.
    std::vector<TransferableResource> list;

    CHECK(child_context_provider_);
    child_resource_provider_->PrepareSendToParent(
        {id1, id2}, &list, child_context_provider_->SharedImageInterface());
    ASSERT_EQ(2u, list.size());
    EXPECT_TRUE(child_resource_provider_->InUseByConsumer(id1));
    EXPECT_TRUE(child_resource_provider_->InUseByConsumer(id2));

    resource_provider_->ReceiveFromChild(child_id, list);

    // In DisplayResourceProvider's namespace, use the mapped resource id.
    std::unordered_map<ResourceId, ResourceId, ResourceIdHasher> resource_map =
        resource_provider_->GetChildToParentMap(child_id);

    scoped_refptr<ResourceFence> fence;
    TestGpuCommandsCompletedFence* gpu_commands_completed_fence = nullptr;
    TestReleaseFence* release_fence = nullptr;
    if (sync_type ==
        TransferableResource::SynchronizationType::kGpuCommandsCompleted) {
      fence = base::MakeRefCounted<TestGpuCommandsCompletedFence>(
          resource_provider_.get());
      gpu_commands_completed_fence =
          static_cast<TestGpuCommandsCompletedFence*>(fence.get());
      resource_provider_->SetGpuCommandsCompletedFence(fence.get());
    } else {
      ASSERT_EQ(TransferableResource::SynchronizationType::kReleaseFence,
                sync_type);
      fence = base::MakeRefCounted<TestReleaseFence>(resource_provider_.get());
      release_fence = static_cast<TestReleaseFence*>(fence.get());
      resource_provider_->SetReleaseFence(fence.get());
    }

    {
      for (auto& resource : list) {
        ResourceId parent_id = resource_map[resource.id];
        lock_set_->LockResource(parent_id, /*maybe_concurrent_reads=*/true,
                                /*is_video_plane=*/false);
      }
      lock_set_->UnlockResources(GenSyncToken());
    }
    EXPECT_EQ(0u, returned_to_child.size());
    EXPECT_EQ(2u, resource_provider_->num_resources());

    resource_provider_->DestroyChild(child_id);

    EXPECT_EQ(0u, resource_provider_->num_resources());
    EXPECT_EQ(2u, returned_to_child.size());

    // id1 should be lost and id2 should not.
    EXPECT_EQ(returned_to_child[0].lost, returned_to_child[0].id == id1);
    EXPECT_EQ(returned_to_child[1].lost, returned_to_child[1].id == id1);

    // fence signalling should be noop.
    if (gpu_commands_completed_fence) {
      gpu_commands_completed_fence->Signal();
    } else {
      gfx::GpuFenceHandle fake_handle;
#if BUILDFLAG(IS_POSIX)
      const int32_t kFenceFd = dup(1);
      fake_handle.Adopt(base::ScopedFD(kFenceFd));
#endif
      release_fence->SetReleaseFence(std::move(fake_handle));
    }

    EXPECT_EQ(0u, resource_provider_->num_resources());
    EXPECT_EQ(2u, returned_to_child.size());

    child_resource_provider_->ReceiveReturnsFromParent(
        std::move(returned_to_child));
    EXPECT_CALL(release, Released(_, _)).Times(2);
    child_resource_provider_->RemoveImportedResource(id1);
    child_resource_provider_->RemoveImportedResource(id2);
  }
}

TEST_F(DisplayResourceProviderSkiaTest, ResourceFenceOutlivesResourceProvider) {
  MockReleaseCallback release;

  TransferableResource tran1 = CreateResource();
  tran1.synchronization_type =
      TransferableResource::SynchronizationType::kGpuCommandsCompleted;
  ResourceId id1 = child_resource_provider_->ImportResource(
      tran1, base::BindOnce(&MockReleaseCallback::Released,
                            base::Unretained(&release)));

  TransferableResource tran2 = CreateResource();
  tran2.synchronization_type =
      TransferableResource::SynchronizationType::kReleaseFence;
  ResourceId id2 = child_resource_provider_->ImportResource(
      tran2, base::BindOnce(&MockReleaseCallback::Released,
                            base::Unretained(&release)));

  std::vector<ReturnedResource> returned_to_child;
  int child_id = resource_provider_->CreateChild(
      GetReturnCallback(&returned_to_child), SurfaceId());

  // Transfer resources to the parent.
  std::vector<TransferableResource> list;

  CHECK(child_context_provider_);
  child_resource_provider_->PrepareSendToParent(
      {id1, id2}, &list, child_context_provider_->SharedImageInterface());
  ASSERT_EQ(2u, list.size());
  EXPECT_TRUE(child_resource_provider_->InUseByConsumer(id1));
  EXPECT_TRUE(child_resource_provider_->InUseByConsumer(id2));

  resource_provider_->ReceiveFromChild(child_id, list);

  // In DisplayResourceProvider's namespace, use the mapped resource id.
  std::unordered_map<ResourceId, ResourceId, ResourceIdHasher> resource_map =
      resource_provider_->GetChildToParentMap(child_id);

  scoped_refptr<TestGpuCommandsCompletedFence> gpu_commands_completed_fence =
      base::MakeRefCounted<TestGpuCommandsCompletedFence>(
          resource_provider_.get());
  resource_provider_->SetGpuCommandsCompletedFence(
      gpu_commands_completed_fence.get());

  scoped_refptr<TestReleaseFence> release_fence =
      base::MakeRefCounted<TestReleaseFence>(resource_provider_.get());
  resource_provider_->SetReleaseFence(release_fence.get());

  {
    for (auto& resource : list) {
      ResourceId parent_id = resource_map[resource.id];
      lock_set_->LockResource(parent_id, /*maybe_concurrent_reads=*/true,
                              /*is_video_plane=*/false);
    }
    lock_set_->UnlockResources(GenSyncToken());
  }
  EXPECT_EQ(0u, returned_to_child.size());
  EXPECT_EQ(2u, resource_provider_->num_resources());

  resource_provider_->DeclareUsedResourcesFromChild(child_id, ResourceIdSet());

  EXPECT_EQ(0u, returned_to_child.size());
  EXPECT_EQ(2u, resource_provider_->num_resources());

  resource_provider_.reset();
  EXPECT_EQ(2u, returned_to_child.size());

  // Signalling the dangling resource fence should not crash.
  if (gpu_commands_completed_fence) {
    gpu_commands_completed_fence->Signal();
  } else {
    gfx::GpuFenceHandle fake_handle;
#if BUILDFLAG(IS_POSIX)
    const int32_t kFenceFd = dup(1);
    fake_handle.Adopt(base::ScopedFD(kFenceFd));
#endif
    release_fence->SetReleaseFence(std::move(fake_handle));
  }

  child_resource_provider_->ReceiveReturnsFromParent(
      std::move(returned_to_child));
  EXPECT_CALL(release, Released(_, _)).Times(2);
  child_resource_provider_->RemoveImportedResource(id1);
  child_resource_provider_->RemoveImportedResource(id2);
}

// Test that ScopedBatchReturnResources batching works.
TEST_F(DisplayResourceProviderSkiaTest,
       ScopedBatchReturnResourcesPreventsReturn) {
  MockReleaseCallback release;

  std::vector<ReturnedResource> returned_to_child;
  int child_id = resource_provider_->CreateChild(
      GetReturnCallback(&returned_to_child), SurfaceId());

  // Transfer some resources to the parent.
  constexpr size_t kTotalResources = 5;
  constexpr size_t kLockedResources = 3;
  constexpr size_t kUsedResources = 4;
  ResourceId ids[kTotalResources];
  for (auto& id : ids) {
    TransferableResource tran = CreateResource();
    id = child_resource_provider_->ImportResource(
        tran, base::BindOnce(&MockReleaseCallback::Released,
                             base::Unretained(&release)));
  }
  std::vector<ResourceId> resource_ids_to_transfer(
      ids, UNSAFE_TODO(ids + kTotalResources));

  std::vector<TransferableResource> list;

  CHECK(child_context_provider_);
  child_resource_provider_->PrepareSendToParent(
      resource_ids_to_transfer, &list,
      child_context_provider_->SharedImageInterface());
  ASSERT_EQ(kTotalResources, list.size());
  for (const auto& id : ids)
    EXPECT_TRUE(child_resource_provider_->InUseByConsumer(id));

  resource_provider_->ReceiveFromChild(child_id, list);

  // In DisplayResourceProvider's namespace, use the mapped resource id.
  std::unordered_map<ResourceId, ResourceId, ResourceIdHasher> resource_map =
      resource_provider_->GetChildToParentMap(child_id);
  std::vector<
      std::unique_ptr<DisplayResourceProvider::ScopedReadLockSharedImage>>
      read_locks;
  for (size_t i = 0; i < kLockedResources; i++) {
    ResourceId mapped_resource_id = resource_map[UNSAFE_TODO(ids[i])];
    lock_set_->LockResource(mapped_resource_id, /*maybe_concurrent_reads=*/true,
                            /*is_video_plane=*/false);
  }

  // Mark all locked resources, and one unlocked resource as used for first
  // batch.
  {
    DisplayResourceProvider::ScopedBatchReturnResources returner(
        resource_provider_.get());
    resource_provider_->DeclareUsedResourcesFromChild(
        child_id, ResourceIdSet(ids, UNSAFE_TODO(ids + kUsedResources)));
    EXPECT_EQ(0u, returned_to_child.size());
  }
  EXPECT_EQ(1u, returned_to_child.size());
  child_resource_provider_->ReceiveReturnsFromParent(
      std::move(returned_to_child));
  returned_to_child.clear();

  // Return all locked resources.
  {
    DisplayResourceProvider::ScopedBatchReturnResources returner(
        resource_provider_.get());
    resource_provider_->DeclareUsedResourcesFromChild(
        child_id, ResourceIdSet(UNSAFE_TODO(ids + kLockedResources),
                                UNSAFE_TODO(ids + kUsedResources)));
    // Can be called multiple times while batching is enabled.  This happens in
    // practice when the same surface is visited using different paths during
    // surface aggregation.
    resource_provider_->DeclareUsedResourcesFromChild(
        child_id, ResourceIdSet(UNSAFE_TODO(ids + kLockedResources),
                                UNSAFE_TODO(ids + kUsedResources)));
    lock_set_->UnlockResources(GenSyncToken());
    EXPECT_EQ(0u, returned_to_child.size());
  }
  EXPECT_EQ(kLockedResources, returned_to_child.size());
  // Returned resources that were locked share the same sync token.
  for (const auto& resource : returned_to_child) {
    EXPECT_TRUE(
        returned_to_child[0].shared_image_export_result.IsEqualForTesting(
            resource.shared_image_export_result));
  }

  child_resource_provider_->ReceiveReturnsFromParent(
      std::move(returned_to_child));
  returned_to_child.clear();

  // Returns from destroying the child is also batched.
  {
    DisplayResourceProvider::ScopedBatchReturnResources returner(
        resource_provider_.get());
    resource_provider_->DestroyChild(child_id);
    EXPECT_EQ(0u, returned_to_child.size());
  }
  EXPECT_EQ(1u, returned_to_child.size());
  child_resource_provider_->ReceiveReturnsFromParent(
      std::move(returned_to_child));
  returned_to_child.clear();

  EXPECT_CALL(release, Released(_, _)).Times(kTotalResources);
  for (const auto& id : ids)
    child_resource_provider_->RemoveImportedResource(id);
}

}  // namespace
}  // namespace viz
