blob: 764ddee72ee5f7fa5b489a4d145b1958e0c751f9 [file]
// 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