blob: e8e73a18e0cbbdeeb5175d25fd05d252d3550a8e [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/viz/common/gpu/metal_api_proxy.h"
#include <objc/objc.h>
#include <map>
#include <string>
#include "base/debug/crash_logging.h"
#include "base/mac/foundation_util.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/synchronization/condition_variable.h"
#include "base/trace_event/trace_event.h"
#include "components/crash/core/common/crash_key.h"
#include "ui/gl/progress_reporter.h"
namespace {
// State shared between the caller of [MTLDevice newLibraryWithSource:] and its
// MTLNewLibraryCompletionHandler (and similarly for -[MTLDevice
// newRenderPipelineStateWithDescriptor:]. The completion handler may be called
// on another thread, so all members are protected by a lock. Accessed via
// scoped_refptr to ensure that it exists until its last accessor is gone.
class AsyncMetalState : public base::RefCountedThreadSafe<AsyncMetalState> {
public:
AsyncMetalState() : condition_variable(&lock) {}
// All members may only be accessed while |lock| is held.
base::Lock lock;
base::ConditionVariable condition_variable;
// Set to true when the completion handler is called.
bool has_result = false;
// The results of the async operation. These are set only by the first
// completion handler to run.
id<MTLLibrary> library = nil;
id<MTLRenderPipelineState> render_pipeline_state = nil;
NSError* error = nil;
private:
friend class base::RefCountedThreadSafe<AsyncMetalState>;
~AsyncMetalState() { DCHECK(has_result); }
};
id<MTLLibrary> NewLibraryWithRetry(id<MTLDevice> device,
NSString* source,
MTLCompileOptions* options,
__autoreleasing NSError** error,
gl::ProgressReporter* progress_reporter) {
SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewLibraryTime");
const base::TimeTicks start_time = base::TimeTicks::Now();
auto state = base::MakeRefCounted<AsyncMetalState>();
// The completion handler will signal the condition variable we will wait
// on. Note that completionHandler will hold a reference to |state|.
MTLNewLibraryCompletionHandler completionHandler =
^(id<MTLLibrary> library, NSError* error) {
base::AutoLock lock(state->lock);
state->has_result = true;
state->library = [library retain];
state->error = [error retain];
state->condition_variable.Signal();
};
// Request asynchronous compilation. Note that |completionHandler| may be
// called from within this function call, or it may be called from a
// different thread.
if (progress_reporter)
progress_reporter->ReportProgress();
[device newLibraryWithSource:source
options:options
completionHandler:completionHandler];
// Suppress the watchdog timer for kTimeout by reporting progress every
// half-second. After that, allow it to kill the the GPU process.
constexpr base::TimeDelta kTimeout = base::TimeDelta::FromSeconds(60);
constexpr base::TimeDelta kWaitPeriod =
base::TimeDelta::FromMilliseconds(500);
while (true) {
if (base::TimeTicks::Now() - start_time < kTimeout && progress_reporter)
progress_reporter->ReportProgress();
base::AutoLock lock(state->lock);
if (state->has_result) {
*error = [state->error autorelease];
return state->library;
}
state->condition_variable.TimedWait(kWaitPeriod);
}
}
id<MTLRenderPipelineState> NewRenderPipelineStateWithRetry(
id<MTLDevice> device,
MTLRenderPipelineDescriptor* descriptor,
__autoreleasing NSError** error,
gl::ProgressReporter* progress_reporter) {
// This function is almost-identical to the above NewLibraryWithRetry. See
// comments in that function.
SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewRenderPipelineStateTime");
const base::TimeTicks start_time = base::TimeTicks::Now();
auto state = base::MakeRefCounted<AsyncMetalState>();
MTLNewRenderPipelineStateCompletionHandler completionHandler =
^(id<MTLRenderPipelineState> render_pipeline_state, NSError* error) {
base::AutoLock lock(state->lock);
state->has_result = true;
state->render_pipeline_state = [render_pipeline_state retain];
state->error = [error retain];
state->condition_variable.Signal();
};
if (progress_reporter)
progress_reporter->ReportProgress();
[device newRenderPipelineStateWithDescriptor:descriptor
completionHandler:completionHandler];
constexpr base::TimeDelta kTimeout = base::TimeDelta::FromSeconds(60);
constexpr base::TimeDelta kWaitPeriod =
base::TimeDelta::FromMilliseconds(500);
while (true) {
if (base::TimeTicks::Now() - start_time < kTimeout && progress_reporter)
progress_reporter->ReportProgress();
base::AutoLock lock(state->lock);
if (state->has_result) {
*error = [state->error autorelease];
return state->render_pipeline_state;
}
state->condition_variable.TimedWait(kWaitPeriod);
}
}
// Maximum length of a shader to be uploaded with a crash report.
constexpr uint32_t kShaderCrashDumpLength = 8128;
} // namespace
// A cache of the result of calls to NewLibraryWithRetry. This will store all
// resulting MTLLibraries indefinitely, and will grow without bound. This is to
// minimize the number of calls hitting the MTLCompilerService, which is prone
// to hangs. Should this significantly help the situation, a more robust (and
// not indefinitely-growing) cache will be added either here or in Skia.
// https://crbug.com/974219
class MTLLibraryCache {
public:
MTLLibraryCache() = default;
~MTLLibraryCache() = default;
id<MTLLibrary> NewLibraryWithSource(id<MTLDevice> device,
NSString* source,
MTLCompileOptions* options,
__autoreleasing NSError** error,
gl::ProgressReporter* progress_reporter) {
LibraryKey key(source, options);
auto found = libraries_.find(key);
if (found != libraries_.end()) {
const LibraryData& data = found->second;
*error = [[data.error retain] autorelease];
return [data.library retain];
}
SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewLibraryTime");
id<MTLLibrary> library =
NewLibraryWithRetry(device, source, options, error, progress_reporter);
LibraryData data(library, *error);
libraries_.insert(std::make_pair(key, std::move(data)));
return library;
}
// The number of cache misses is the number of times that we have had to call
// the true newLibraryWithSource function.
uint64_t CacheMissCount() const { return libraries_.size(); }
private:
struct LibraryKey {
LibraryKey(NSString* source, MTLCompileOptions* options)
: source_(source, base::scoped_policy::RETAIN),
options_(options, base::scoped_policy::RETAIN) {}
LibraryKey(const LibraryKey& other) = default;
LibraryKey& operator=(const LibraryKey& other) = default;
~LibraryKey() = default;
bool operator<(const LibraryKey& other) const {
switch ([source_ compare:other.source_]) {
case NSOrderedAscending:
return true;
case NSOrderedDescending:
return false;
case NSOrderedSame:
break;
}
#define COMPARE(x) \
if ([options_ x] < [other.options_ x]) \
return true; \
if ([options_ x] > [other.options_ x]) \
return false;
COMPARE(fastMathEnabled);
COMPARE(languageVersion);
#undef COMPARE
// Skia doesn't set any preprocessor macros, and defining an order on two
// NSDictionaries is a lot of code, so just assert that there are no
// macros. Should this alleviate https://crbug.com/974219, then a more
// robust cache should be implemented.
DCHECK_EQ([[options_ preprocessorMacros] count], 0u);
return false;
}
private:
base::scoped_nsobject<NSString> source_;
base::scoped_nsobject<MTLCompileOptions> options_;
};
struct LibraryData {
LibraryData(id<MTLLibrary> library_, NSError* error_)
: library(library_, base::scoped_policy::RETAIN),
error(error_, base::scoped_policy::RETAIN) {}
LibraryData(const LibraryData& other) = default;
LibraryData& operator=(const LibraryData& other) = default;
~LibraryData() = default;
base::scoped_nsprotocol<id<MTLLibrary>> library;
base::scoped_nsobject<NSError> error;
};
std::map<LibraryKey, LibraryData> libraries_;
DISALLOW_COPY_AND_ASSIGN(MTLLibraryCache);
};
// Disable protocol warnings and property synthesis warnings. Any unimplemented
// methods/properties in the MTLDevice protocol will be handled by the
// -forwardInvocation: method.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wprotocol"
#pragma clang diagnostic ignored "-Wobjc-protocol-property-synthesis"
@implementation MTLDeviceProxy
- (id)initWithDevice:(id<MTLDevice>)device {
if (self = [super init]) {
_device.reset(device, base::scoped_policy::RETAIN);
_libraryCache = std::make_unique<MTLLibraryCache>();
}
return self;
}
- (void)setProgressReporter:(gl::ProgressReporter*)progressReporter {
_progressReporter = progressReporter;
}
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
// Technically, _device is of protocol MTLDevice which inherits from protocol
// NSObject, and protocol NSObject does not have -methodSignatureForSelector:.
// Assume that the implementing class derives from NSObject.
return [base::mac::ObjCCastStrict<NSObject>(_device)
methodSignatureForSelector:selector];
}
- (void)forwardInvocation:(NSInvocation*)invocation {
// The number of methods on MTLDevice is finite and small, so this unbounded
// cache is fine. std::map does not move elements on additions to the map, so
// the requirement that strings passed to TRACE_EVENT0 don't move is
// fulfilled.
static base::NoDestructor<std::map<SEL, std::string>> invocationNames;
auto& invocationName = (*invocationNames)[invocation.selector];
if (invocationName.empty()) {
invocationName =
base::StringPrintf("-[MTLDevice %s]", sel_getName(invocation.selector));
}
TRACE_EVENT0("gpu", invocationName.c_str());
gl::ScopedProgressReporter scoped_reporter(_progressReporter);
[invocation invokeWithTarget:_device.get()];
}
- (nullable id<MTLLibrary>)
newLibraryWithSource:(NSString*)source
options:(nullable MTLCompileOptions*)options
error:(__autoreleasing NSError**)error {
TRACE_EVENT0("gpu", "-[MTLDevice newLibraryWithSource:options:error:]");
// Capture the shader's source in a crash key in case newLibraryWithSource
// hangs.
// https://crbug.com/974219
static crash_reporter::CrashKeyString<kShaderCrashDumpLength> shaderKey(
"MTLShaderSource");
std::string sourceAsSysString = base::SysNSStringToUTF8(source);
if (sourceAsSysString.size() > kShaderCrashDumpLength)
DLOG(WARNING) << "Truncating shader in crash log.";
shaderKey.Set(sourceAsSysString);
static crash_reporter::CrashKeyString<16> newLibraryCountKey(
"MTLNewLibraryCount");
newLibraryCountKey.Set(base::NumberToString(_libraryCache->CacheMissCount()));
id<MTLLibrary> library = _libraryCache->NewLibraryWithSource(
_device, source, options, error, _progressReporter);
shaderKey.Clear();
newLibraryCountKey.Clear();
// Shaders from Skia will have either a vertexMain or fragmentMain function.
// Save the source and a weak pointer to the function, so we can capture
// the shader source in -newRenderPipelineStateWithDescriptor (see further
// remarks in that function).
base::scoped_nsprotocol<id<MTLFunction>> vertexFunction(
[library newFunctionWithName:@"vertexMain"]);
if (vertexFunction) {
_vertexSourceFunction = vertexFunction;
_vertexSource = sourceAsSysString;
}
base::scoped_nsprotocol<id<MTLFunction>> fragmentFunction(
[library newFunctionWithName:@"fragmentMain"]);
if (fragmentFunction) {
_fragmentSourceFunction = fragmentFunction;
_fragmentSource = sourceAsSysString;
}
return library;
}
- (nullable id<MTLRenderPipelineState>)
newRenderPipelineStateWithDescriptor:
(MTLRenderPipelineDescriptor*)descriptor
error:(__autoreleasing NSError**)error {
TRACE_EVENT0("gpu",
"-[MTLDevice newRenderPipelineStateWithDescriptor:error:]");
// Capture the vertex and shader source being used. Skia's use pattern is to
// compile two MTLLibraries before creating a MTLRenderPipelineState -- one
// with vertexMain and the other with fragmentMain. The two immediately
// previous -newLibraryWithSource calls should have saved the sources for
// these two functions.
// https://crbug.com/974219
static crash_reporter::CrashKeyString<kShaderCrashDumpLength> vertexShaderKey(
"MTLVertexSource");
if (_vertexSourceFunction == [descriptor vertexFunction])
vertexShaderKey.Set(_vertexSource);
else
DLOG(WARNING) << "Failed to capture vertex shader.";
static crash_reporter::CrashKeyString<kShaderCrashDumpLength>
fragmentShaderKey("MTLFragmentSource");
if (_fragmentSourceFunction == [descriptor fragmentFunction])
fragmentShaderKey.Set(_fragmentSource);
else
DLOG(WARNING) << "Failed to capture fragment shader.";
static crash_reporter::CrashKeyString<16> newLibraryCountKey(
"MTLNewLibraryCount");
newLibraryCountKey.Set(base::NumberToString(_libraryCache->CacheMissCount()));
SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewRenderPipelineStateTime");
id<MTLRenderPipelineState> pipelineState = NewRenderPipelineStateWithRetry(
_device, descriptor, error, _progressReporter);
vertexShaderKey.Clear();
fragmentShaderKey.Clear();
newLibraryCountKey.Clear();
return pipelineState;
}
@end
#pragma clang diagnostic pop