| // Copyright 2025 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "gpu/command_buffer/service/graphite_shared_context.h" | |
| #include "base/functional/bind.h" | |
| #include "base/memory/raw_ptr.h" | |
| #include "base/test/task_environment.h" | |
| #include "base/test/test_future.h" | |
| #include "base/threading/thread.h" | |
| #include "base/time/time.h" | |
| #include "build/build_config.h" | |
| #include "gpu/command_buffer/common/shm_count.h" | |
| #include "gpu/command_buffer/service/skia_utils.h" | |
| #include "skia/buildflags.h" | |
| #include "testing/gmock/include/gmock/gmock.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| #include "third_party/dawn/include/dawn/dawn_proc.h" | |
| #include "third_party/dawn/include/dawn/native/DawnNative.h" // nogncheck | |
| #include "third_party/skia/include/core/SkCanvas.h" | |
| #include "third_party/skia/include/gpu/graphite/Context.h" | |
| #include "third_party/skia/include/gpu/graphite/ContextOptions.h" | |
| #include "third_party/skia/include/gpu/graphite/PrecompileContext.h" | |
| #include "third_party/skia/include/gpu/graphite/Recording.h" | |
| #include "third_party/skia/include/gpu/graphite/Surface.h" | |
| #include "third_party/skia/include/gpu/graphite/dawn/DawnBackendContext.h" | |
| namespace gpu { | |
| namespace { | |
| using testing::NiceMock; | |
| constexpr size_t kMaxPendingRecordings = 100; | |
| constexpr base::TimeDelta kMaxTimeBetweenSubmits = base::Milliseconds(500); | |
| class MockGpuProcessShmCount : public GpuProcessShmCount { | |
| public: | |
| MOCK_METHOD(void, Increment, (), (override)); | |
| MOCK_METHOD(void, Decrement, (), (override)); | |
| }; | |
| class MockDelegate : public GraphiteSharedContext::Delegate { | |
| public: | |
| MOCK_METHOD(void, FlushBackend, (), (override)); | |
| MOCK_METHOD(void, | |
| MarkContextLost, | |
| (error::ContextLostReason reason), | |
| (override)); | |
| MOCK_METHOD(bool, IsContextLost, (), (const, override)); | |
| MOCK_METHOD(void, ReportProgress, (), (override)); | |
| MOCK_METHOD(base::OnceClosure, CreateSubmitCleanupCallback, (), (override)); | |
| }; | |
| // Test fixture for GraphiteSharedContext with thread safety enabled. | |
| class GraphiteSharedContextTest : public testing::TestWithParam<bool> { | |
| protected: | |
| GraphiteSharedContextTest() { | |
| InitializeGraphiteDawn(); | |
| if (is_thread_safe()) { | |
| secondary_thread_ = std::make_unique<base::Thread>("Secondary_Thread"); | |
| secondary_thread_->StartAndWaitForTesting(); | |
| } | |
| } | |
| ~GraphiteSharedContextTest() { | |
| if (secondary_thread_) { | |
| secondary_thread_->Stop(); | |
| } | |
| } | |
| bool is_thread_safe() const { return GetParam(); } | |
| void InitializeGraphiteDawn() { | |
| dawnProcSetProcs(&dawn::native::GetProcs()); | |
| wgpu::InstanceDescriptor instance_desc = {}; | |
| static constexpr auto kTimedWaitAny = | |
| wgpu::InstanceFeatureName::TimedWaitAny; | |
| instance_desc.requiredFeatureCount = 1; | |
| instance_desc.requiredFeatures = &kTimedWaitAny; | |
| dawn::native::Instance dawn_instance(&instance_desc); | |
| wgpu::RequestAdapterOptions options = {}; | |
| #if BUILDFLAG(IS_APPLE) | |
| options.backendType = wgpu::BackendType::Metal; | |
| #elif BUILDFLAG(IS_WIN) | |
| options.backendType = wgpu::BackendType::D3D11; | |
| #else | |
| // Android, ChromeOS, Fuchsia, Linux all use Vulkan. | |
| options.backendType = wgpu::BackendType::Vulkan; | |
| // Force Swiftshader on Linux due to threading issues with native drivers. | |
| options.forceFallbackAdapter = !!BUILDFLAG(IS_LINUX); | |
| #endif | |
| options.featureLevel = wgpu::FeatureLevel::Core; | |
| std::vector<dawn::native::Adapter> adapters = | |
| dawn_instance.EnumerateAdapters(&options); | |
| CHECK(!adapters.empty()); | |
| wgpu::DeviceDescriptor device_desc = {}; | |
| auto device = wgpu::Adapter(adapters[0].Get()).CreateDevice(&device_desc); | |
| CHECK(device); | |
| skgpu::graphite::DawnBackendContext backend_context = {}; | |
| backend_context.fInstance = wgpu::Instance(dawn_instance.Get()); | |
| backend_context.fDevice = device; | |
| backend_context.fQueue = device.GetQueue(); | |
| // Use the default Graphite context options that Chromium uses e.g. disallow | |
| // things like out of order recordings. | |
| gpu::GpuDriverBugWorkarounds workarounds; | |
| auto context_options = GetDefaultGraphiteContextOptions(workarounds); | |
| graphite_shared_context_ = std::make_unique<GraphiteSharedContext>( | |
| skgpu::graphite::ContextFactory::MakeDawn(backend_context, | |
| context_options), | |
| &use_shader_cache_shm_count_, is_thread_safe(), kMaxPendingRecordings, | |
| kMaxTimeBetweenSubmits, &delegate_); | |
| } | |
| void InsertEmptyRecording(skgpu::graphite::Recorder* recorder) { | |
| auto recording = recorder->snap(); | |
| ASSERT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_TRUE(graphite_shared_context_->insertRecording(info)); | |
| } | |
| base::test::TaskEnvironment task_environment_{ | |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; | |
| MockGpuProcessShmCount use_shader_cache_shm_count_; | |
| NiceMock<MockDelegate> delegate_; | |
| std::unique_ptr<GraphiteSharedContext> graphite_shared_context_; | |
| std::unique_ptr<base::Thread> secondary_thread_; | |
| }; | |
| TEST_P(GraphiteSharedContextTest, IsThreadSafe) { | |
| // GraphiteSharedContext graphite_shared_context_ is create with | |
| // |is_thread_safe| = true in SetUp(). |lock_| should be allocated and | |
| // IsThreadSafe() is read back as true. | |
| EXPECT_EQ(graphite_shared_context_->IsThreadSafe(), is_thread_safe()); | |
| } | |
| // Test that multiple threads can safely call methods on GraphiteSharedContext. | |
| TEST_P(GraphiteSharedContextTest, ConcurrentAccess) { | |
| if (!is_thread_safe()) { | |
| GTEST_SKIP() << "Concurrent access only supported with thread safe context"; | |
| } | |
| // Warming up the secondary thread with a no-op function. | |
| secondary_thread_->task_runner()->PostTask(FROM_HERE, base::BindOnce([]() { | |
| // Do nothing. | |
| })); | |
| // Flush and wait until it completes | |
| secondary_thread_->FlushForTesting(); | |
| auto run_graphite_functions = | |
| [](GraphiteSharedContext* graphite_shared_context) { | |
| // Call a method that acquires the lock | |
| auto recorder = graphite_shared_context->makeRecorder(); | |
| EXPECT_TRUE(recorder); | |
| for (int i = 0; i < 2; ++i) { | |
| for (int j = 0; j < 10; ++j) { | |
| auto recording = recorder->snap(); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_TRUE(recording); | |
| bool insert_success = | |
| graphite_shared_context->insertRecording(info); | |
| EXPECT_TRUE(insert_success); | |
| } | |
| graphite_shared_context->submit(); | |
| } | |
| EXPECT_FALSE(graphite_shared_context->isDeviceLost()); | |
| }; | |
| // Call graphite::context functions on the secondary thread. | |
| secondary_thread_->task_runner()->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(run_graphite_functions, | |
| base::Unretained(graphite_shared_context_.get()))); | |
| // Call graphite::context functions at the same time on the main thread. | |
| // We should not encounter any failures or deadlocks on either threads. | |
| run_graphite_functions(graphite_shared_context_.get()); | |
| // Just in case the secondary thread is slower, Wait until finished before | |
| // exit. | |
| secondary_thread_->FlushForTesting(); | |
| } | |
| TEST_P(GraphiteSharedContextTest, AsyncShaderCompilesFailed) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| EXPECT_TRUE(recorder); | |
| auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); | |
| auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface1->getCanvas()->clear(SK_ColorRED); | |
| auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0); | |
| auto recording = recorder->snap(); | |
| EXPECT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(false)); | |
| EXPECT_CALL(use_shader_cache_shm_count_, Increment()).Times(1); | |
| EXPECT_CALL(use_shader_cache_shm_count_, Decrement()).Times(1); | |
| graphite_shared_context_->set_simulated_insert_status( | |
| skgpu::graphite::InsertStatus::kAsyncShaderCompilesFailed); | |
| EXPECT_FALSE(graphite_shared_context_->insertRecording(info)); | |
| } | |
| TEST_P(GraphiteSharedContextTest, AsyncShaderCompilesFailedContextLost) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| EXPECT_TRUE(recorder); | |
| auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); | |
| auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface1->getCanvas()->clear(SK_ColorRED); | |
| auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0); | |
| auto recording = recorder->snap(); | |
| EXPECT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(true)); | |
| EXPECT_CALL(use_shader_cache_shm_count_, Increment()).Times(0); | |
| EXPECT_CALL(use_shader_cache_shm_count_, Decrement()).Times(0); | |
| graphite_shared_context_->set_simulated_insert_status( | |
| skgpu::graphite::InsertStatus::kAsyncShaderCompilesFailed); | |
| EXPECT_FALSE(graphite_shared_context_->insertRecording(info)); | |
| } | |
| TEST_P(GraphiteSharedContextTest, InsertRecordingContextLost) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| EXPECT_TRUE(recorder); | |
| auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); | |
| auto surface = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface->getCanvas()->clear(SK_ColorRED); | |
| auto recording = recorder->snap(); | |
| EXPECT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(true)); | |
| EXPECT_FALSE(graphite_shared_context_->insertRecording(info)); | |
| } | |
| #if BUILDFLAG(IS_WIN) | |
| // Verifies that OOM on the first diagnostic line loses the context without | |
| // invalidating the shader cache, while later-line mentions are ignored. | |
| TEST_P(GraphiteSharedContextTest, AsyncShaderCompilesFailedOutOfMemory) { | |
| struct TestCase { | |
| const char* message; | |
| bool out_of_memory; | |
| }; | |
| constexpr TestCase kTestCases[] = { | |
| {"FXC compile failed with error: E_OUTOFMEMORY.", true}, | |
| {"FXC compile failed with error: E_OUTOFMEMORY msg: out of memory during " | |
| "compilation\n/* Generated HLSL: */\n", | |
| true}, | |
| {"DXC compile failed with error: E_OUTOFMEMORY.", true}, | |
| {"DXC compile failed with error: E_OUTOFMEMORY msg: out of memory\n" | |
| "/* Generated HLSL: */\n", | |
| true}, | |
| {"DXC compile failed with E_OUTOFMEMORY (0x8007000E)", true}, | |
| {"DXC get status failed with E_OUTOFMEMORY (0x8007000E)", true}, | |
| {"DXC get error buffer failed with E_OUTOFMEMORY (0x8007000E)", true}, | |
| {"DXC get result failed with E_OUTOFMEMORY (0x8007000E)", true}, | |
| {"FXC compile failed with error: E_FAIL msg: compilation failed\n" | |
| "/* Generated HLSL: */\n" | |
| "// FXC compile failed with error: E_OUTOFMEMORY", | |
| false}, | |
| {"DXC compile failed with error: E_FAIL.", false}, | |
| {"DXC compile failed with E_FAIL (0x80004005)", false}, | |
| {"Unknown compiler failed with E_OUTOFMEMORY", true}, | |
| {"DXC compile failed with E_FAIL (0x80004005)\nE_OUTOFMEMORY", false}, | |
| {"\nE_OUTOFMEMORY", false}, | |
| {"", false}, | |
| }; | |
| for (const auto& test_case : kTestCases) { | |
| SCOPED_TRACE(test_case.message); | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| ASSERT_TRUE(recorder); | |
| auto recording = recorder->snap(); | |
| ASSERT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(false)); | |
| EXPECT_CALL(delegate_, MarkContextLost(error::kOutOfMemory)) | |
| .Times(test_case.out_of_memory ? 1 : 0); | |
| EXPECT_CALL(use_shader_cache_shm_count_, Increment()) | |
| .Times(test_case.out_of_memory ? 0 : 1); | |
| EXPECT_CALL(use_shader_cache_shm_count_, Decrement()) | |
| .Times(test_case.out_of_memory ? 0 : 1); | |
| graphite_shared_context_->set_simulated_insert_status( | |
| {skgpu::graphite::InsertStatus::kAsyncShaderCompilesFailed, | |
| test_case.message}); | |
| EXPECT_FALSE(graphite_shared_context_->insertRecording(info)); | |
| testing::Mock::VerifyAndClearExpectations(&delegate_); | |
| testing::Mock::VerifyAndClearExpectations(&use_shader_cache_shm_count_); | |
| } | |
| } | |
| #endif // BUILDFLAG(IS_WIN) | |
| TEST_P(GraphiteSharedContextTest, OutOfOrderRecording) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| EXPECT_TRUE(recorder); | |
| auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); | |
| auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface1->getCanvas()->clear(SK_ColorRED); | |
| auto recording1 = recorder->snap(); | |
| EXPECT_TRUE(recording1); | |
| auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0); | |
| auto recording2 = recorder->snap(); | |
| EXPECT_TRUE(recording2); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording2.get(); | |
| graphite_shared_context_->insertRecording(info); | |
| info.fRecording = recording1.get(); | |
| EXPECT_CALL(delegate_, MarkContextLost(testing::_)).Times(1); | |
| EXPECT_FALSE(graphite_shared_context_->insertRecording(info)); | |
| } | |
| TEST_P(GraphiteSharedContextTest, AddCommandsFailed) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| EXPECT_TRUE(recorder); | |
| auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); | |
| auto surface1 = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface1->getCanvas()->clear(SK_ColorRED); | |
| auto surface2 = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface2->getCanvas()->drawImage(surface1->makeTemporaryImage(), 0, 0); | |
| auto recording = recorder->snap(); | |
| EXPECT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_CALL(use_shader_cache_shm_count_, Increment()).Times(0); | |
| EXPECT_CALL(use_shader_cache_shm_count_, Decrement()).Times(0); | |
| graphite_shared_context_->set_simulated_insert_status( | |
| skgpu::graphite::InsertStatus::kAddCommandsFailed); | |
| EXPECT_FALSE(graphite_shared_context_->insertRecording(info)); | |
| } | |
| TEST_P(GraphiteSharedContextTest, LowPendingRecordings) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| EXPECT_TRUE(recorder); | |
| // No flush is expected if the number of pending recordings is low. | |
| EXPECT_CALL(delegate_, FlushBackend()).Times(0); | |
| for (size_t i = 0; i < kMaxPendingRecordings - 1; ++i) { | |
| auto recording = recorder->snap(); | |
| EXPECT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_TRUE(graphite_shared_context_->insertRecording(info)); | |
| } | |
| } | |
| TEST_P(GraphiteSharedContextTest, MaxPendingRecordings) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| EXPECT_TRUE(recorder); | |
| // Expect a flush when the number of pending recordings reaches the max. | |
| EXPECT_CALL(delegate_, FlushBackend()).Times(1); | |
| for (size_t i = 0; i < kMaxPendingRecordings; ++i) { | |
| auto recording = recorder->snap(); | |
| EXPECT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_TRUE(graphite_shared_context_->insertRecording(info)); | |
| } | |
| } | |
| // Inserting a recording once the max time between submits has passed forces a | |
| // submit. | |
| TEST_P(GraphiteSharedContextTest, TimeBasedSubmit) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| ASSERT_TRUE(recorder); | |
| // Start the time since the last submit. | |
| graphite_shared_context_->submit(); | |
| EXPECT_CALL(delegate_, FlushBackend()).Times(1); | |
| task_environment_.AdvanceClock(kMaxTimeBetweenSubmits); | |
| // The recording arrives once the max time has passed, so it forces a submit. | |
| InsertEmptyRecording(recorder.get()); | |
| } | |
| // Inserting a recording before the max time between submits has passed doesn't | |
| // force a submit. | |
| TEST_P(GraphiteSharedContextTest, NoTimeBasedSubmitBeforeMaxTime) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| ASSERT_TRUE(recorder); | |
| // Start the time since the last submit. | |
| graphite_shared_context_->submit(); | |
| EXPECT_CALL(delegate_, FlushBackend()).Times(0); | |
| task_environment_.AdvanceClock(kMaxTimeBetweenSubmits - | |
| base::Milliseconds(1)); | |
| // The recording arrives just before the max time, so it doesn't force a | |
| // submit. | |
| InsertEmptyRecording(recorder.get()); | |
| } | |
| // Test that async read pixels callbacks are skipped when the context is lost. | |
| TEST_P(GraphiteSharedContextTest, ReadPixelsContextLost) { | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| EXPECT_TRUE(recorder); | |
| auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); | |
| auto surface = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface->getCanvas()->clear(SK_ColorRED); | |
| auto recording = recorder->snap(); | |
| EXPECT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_TRUE(graphite_shared_context_->insertRecording(info)); | |
| base::test::TestFuture<void*, | |
| std::unique_ptr<const SkSurface::AsyncReadResult>> | |
| readback_future; | |
| // When context is not lost, the callback should be invoked. | |
| EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(false)); | |
| EXPECT_TRUE(graphite_shared_context_->asyncRescaleAndReadPixelsAndSubmit( | |
| surface.get(), ii, SkIRect::MakeWH(64, 64), SkImage::RescaleGamma::kSrc, | |
| SkImage::RescaleMode::kNearest, readback_future.GetCallback(), nullptr)); | |
| EXPECT_TRUE(readback_future.IsReady()); | |
| // When context is lost, the callback should not be invoked. | |
| readback_future.Clear(); | |
| EXPECT_CALL(delegate_, IsContextLost()).WillOnce(testing::Return(true)); | |
| EXPECT_TRUE(graphite_shared_context_->asyncRescaleAndReadPixelsAndSubmit( | |
| surface.get(), ii, SkIRect::MakeWH(64, 64), SkImage::RescaleGamma::kSrc, | |
| SkImage::RescaleMode::kNearest, readback_future.GetCallback(), nullptr)); | |
| EXPECT_FALSE(readback_future.IsReady()); | |
| } | |
| // Test that the delegate's cleanup callback runs once submitted work finishes. | |
| TEST_P(GraphiteSharedContextTest, SubmitRunsCleanupCallback) { | |
| if (is_thread_safe()) { | |
| GTEST_SKIP() | |
| << "Cleanup callbacks only supported when context is not thread safe"; | |
| } | |
| auto recorder = graphite_shared_context_->makeRecorder(); | |
| ASSERT_TRUE(recorder); | |
| auto ii = SkImageInfo::Make(64, 64, kN32_SkColorType, kPremul_SkAlphaType); | |
| auto surface = SkSurfaces::RenderTarget(recorder.get(), ii); | |
| surface->getCanvas()->clear(SK_ColorRED); | |
| auto recording = recorder->snap(); | |
| ASSERT_TRUE(recording); | |
| skgpu::graphite::InsertRecordingInfo info = {}; | |
| info.fRecording = recording.get(); | |
| EXPECT_TRUE(graphite_shared_context_->insertRecording(info)); | |
| bool cleanup_ran = false; | |
| EXPECT_CALL(delegate_, CreateSubmitCleanupCallback()) | |
| .WillOnce([&cleanup_ran]() { | |
| return base::BindOnce([](bool* ran) { *ran = true; }, &cleanup_ran); | |
| }); | |
| graphite_shared_context_->submit(skgpu::graphite::SyncToCpu::kYes); | |
| EXPECT_TRUE(cleanup_ran); | |
| } | |
| // Test that a cleanup callback forces a submit even when there is no pending | |
| // GPU work, so deferred cleanup still makes progress. | |
| TEST_P(GraphiteSharedContextTest, CleanupCallbackWithoutPendingWork) { | |
| if (is_thread_safe()) { | |
| GTEST_SKIP() | |
| << "Cleanup callbacks only supported when context is not thread safe"; | |
| } | |
| bool cleanup_ran = false; | |
| EXPECT_CALL(delegate_, CreateSubmitCleanupCallback()) | |
| .WillOnce([&cleanup_ran]() { | |
| return base::BindOnce([](bool* ran) { *ran = true; }, &cleanup_ran); | |
| }) | |
| .WillRepeatedly([]() { return base::OnceClosure(); }); | |
| graphite_shared_context_->submit(skgpu::graphite::SyncToCpu::kNo); | |
| // Wait for all outstanding GPU work, which processes finished procs. | |
| graphite_shared_context_->submit(skgpu::graphite::SyncToCpu::kYes); | |
| EXPECT_TRUE(cleanup_ran); | |
| } | |
| // Test that the cleanup callback is chained with a caller provided finished | |
| // proc and that both run, with the finished proc running first. | |
| TEST_P(GraphiteSharedContextTest, CleanupCallbackChainsFinishedProc) { | |
| if (is_thread_safe()) { | |
| GTEST_SKIP() | |
| << "Cleanup callbacks only supported when context is not thread safe"; | |
| } | |
| struct FinishedState { | |
| bool finished_proc_ran = false; | |
| bool cleanup_ran = false; | |
| bool finished_proc_ran_first = false; | |
| } state; | |
| EXPECT_CALL(delegate_, CreateSubmitCleanupCallback()).WillOnce([&state]() { | |
| return base::BindOnce( | |
| [](FinishedState* state) { | |
| state->cleanup_ran = true; | |
| state->finished_proc_ran_first = state->finished_proc_ran; | |
| }, | |
| base::Unretained(&state)); | |
| }); | |
| skgpu::graphite::SubmitInfo submit_info(skgpu::graphite::SyncToCpu::kYes); | |
| submit_info.fFinishedContext = &state; | |
| submit_info.fFinishedProc = [](void* ctx, skgpu::CallbackResult) { | |
| static_cast<FinishedState*>(ctx)->finished_proc_ran = true; | |
| }; | |
| graphite_shared_context_->submit(submit_info); | |
| EXPECT_TRUE(state.finished_proc_ran); | |
| EXPECT_TRUE(state.cleanup_ran); | |
| EXPECT_TRUE(state.finished_proc_ran_first); | |
| } | |
| INSTANTIATE_TEST_SUITE_P(, GraphiteSharedContextTest, testing::Bool()); | |
| } // namespace | |
| } // namespace gpu |