1// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/viz/common/gpu/metal_api_proxy.h"
6
7#include <objc/objc.h>
8
9#include <map>
10#include <string>
11
12#include "base/debug/crash_logging.h"
13#include "base/mac/foundation_util.h"
14#include "base/memory/ref_counted.h"
15#include "base/metrics/histogram_macros.h"
16#include "base/no_destructor.h"
17#include "base/strings/string_number_conversions.h"
18#include "base/strings/stringprintf.h"
19#include "base/strings/sys_string_conversions.h"
20#include "base/synchronization/condition_variable.h"
21#include "base/trace_event/trace_event.h"
22#include "components/crash/core/common/crash_key.h"
23#include "ui/gl/progress_reporter.h"
24
25namespace {
26
27// State shared between the caller of [MTLDevice newLibraryWithSource:] and its
28// MTLNewLibraryCompletionHandler (and similarly for -[MTLDevice
29// newRenderPipelineStateWithDescriptor:]. The completion handler may be called
30// on another thread, so all members are protected by a lock. Accessed via
31// scoped_refptr to ensure that it exists until its last accessor is gone.
32class API_AVAILABLE(macos(10.11)) AsyncMetalState
33    : public base::RefCountedThreadSafe<AsyncMetalState> {
34 public:
35  AsyncMetalState() : condition_variable(&lock) {}
36
37  // All members may only be accessed while |lock| is held.
38  base::Lock lock;
39  base::ConditionVariable condition_variable;
40
41  // Set to true when the completion handler is called.
42  bool has_result = false;
43
44  // The results of the async operation. These are set only by the first
45  // completion handler to run.
46  id<MTLLibrary> library = nil;
47  id<MTLRenderPipelineState> render_pipeline_state = nil;
48  NSError* error = nil;
49
50 private:
51  friend class base::RefCountedThreadSafe<AsyncMetalState>;
52  ~AsyncMetalState() { DCHECK(has_result); }
53};
54
55id<MTLLibrary> API_AVAILABLE(macos(10.11))
56    NewLibraryWithRetry(id<MTLDevice> device,
57                        NSString* source,
58                        MTLCompileOptions* options,
59                        __autoreleasing NSError** error,
60                        gl::ProgressReporter* progress_reporter) {
61  SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewLibraryTime");
62  const base::TimeTicks start_time = base::TimeTicks::Now();
63  auto state = base::MakeRefCounted<AsyncMetalState>();
64
65  // The completion handler will signal the condition variable we will wait
66  // on. Note that completionHandler will hold a reference to |state|.
67  MTLNewLibraryCompletionHandler completionHandler =
68      ^(id<MTLLibrary> library, NSError* error) {
69        base::AutoLock lock(state->lock);
70        state->has_result = true;
71        state->library = [library retain];
72        state->error = [error retain];
73        state->condition_variable.Signal();
74      };
75
76  // Request asynchronous compilation. Note that |completionHandler| may be
77  // called from within this function call, or it may be called from a
78  // different thread.
79  if (progress_reporter)
80    progress_reporter->ReportProgress();
81  [device newLibraryWithSource:source
82                       options:options
83             completionHandler:completionHandler];
84
85  // Suppress the watchdog timer for kTimeout by reporting progress every
86  // half-second. After that, allow it to kill the the GPU process.
87  constexpr base::TimeDelta kTimeout = base::TimeDelta::FromSeconds(60);
88  constexpr base::TimeDelta kWaitPeriod =
89      base::TimeDelta::FromMilliseconds(500);
90  while (true) {
91    if (base::TimeTicks::Now() - start_time < kTimeout && progress_reporter)
92      progress_reporter->ReportProgress();
93    base::AutoLock lock(state->lock);
94    if (state->has_result) {
95      *error = [state->error autorelease];
96      return state->library;
97    }
98    state->condition_variable.TimedWait(kWaitPeriod);
99  }
100}
101
102id<MTLRenderPipelineState> API_AVAILABLE(macos(10.11))
103    NewRenderPipelineStateWithRetry(id<MTLDevice> device,
104                                    MTLRenderPipelineDescriptor* descriptor,
105                                    __autoreleasing NSError** error,
106                                    gl::ProgressReporter* progress_reporter) {
107  // This function is almost-identical to the above NewLibraryWithRetry. See
108  // comments in that function.
109  SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewRenderPipelineStateTime");
110  const base::TimeTicks start_time = base::TimeTicks::Now();
111  auto state = base::MakeRefCounted<AsyncMetalState>();
112  MTLNewRenderPipelineStateCompletionHandler completionHandler =
113      ^(id<MTLRenderPipelineState> render_pipeline_state, NSError* error) {
114        base::AutoLock lock(state->lock);
115        state->has_result = true;
116        state->render_pipeline_state = [render_pipeline_state retain];
117        state->error = [error retain];
118        state->condition_variable.Signal();
119      };
120  if (progress_reporter)
121    progress_reporter->ReportProgress();
122  [device newRenderPipelineStateWithDescriptor:descriptor
123                             completionHandler:completionHandler];
124  constexpr base::TimeDelta kTimeout = base::TimeDelta::FromSeconds(60);
125  constexpr base::TimeDelta kWaitPeriod =
126      base::TimeDelta::FromMilliseconds(500);
127  while (true) {
128    if (base::TimeTicks::Now() - start_time < kTimeout && progress_reporter)
129      progress_reporter->ReportProgress();
130    base::AutoLock lock(state->lock);
131    if (state->has_result) {
132      *error = [state->error autorelease];
133      return state->render_pipeline_state;
134    }
135    state->condition_variable.TimedWait(kWaitPeriod);
136  }
137}
138
139// Maximum length of a shader to be uploaded with a crash report.
140constexpr uint32_t kShaderCrashDumpLength = 8128;
141
142}  // namespace
143
144// A cache of the result of calls to NewLibraryWithRetry. This will store all
145// resulting MTLLibraries indefinitely, and will grow without bound. This is to
146// minimize the number of calls hitting the MTLCompilerService, which is prone
147// to hangs. Should this significantly help the situation, a more robust (and
148// not indefinitely-growing) cache will be added either here or in Skia.
149// https://crbug.com/974219
150class API_AVAILABLE(macos(10.11)) MTLLibraryCache {
151 public:
152  MTLLibraryCache() = default;
153  ~MTLLibraryCache() = default;
154
155  id<MTLLibrary> NewLibraryWithSource(id<MTLDevice> device,
156                                      NSString* source,
157                                      MTLCompileOptions* options,
158                                      __autoreleasing NSError** error,
159                                      gl::ProgressReporter* progress_reporter) {
160    LibraryKey key(source, options);
161    auto found = libraries_.find(key);
162    if (found != libraries_.end()) {
163      const LibraryData& data = found->second;
164      *error = [[data.error retain] autorelease];
165      return [data.library retain];
166    }
167    SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewLibraryTime");
168    id<MTLLibrary> library =
169        NewLibraryWithRetry(device, source, options, error, progress_reporter);
170    LibraryData data(library, *error);
171    libraries_.insert(std::make_pair(key, std::move(data)));
172    return library;
173  }
174  // The number of cache misses is the number of times that we have had to call
175  // the true newLibraryWithSource function.
176  uint64_t CacheMissCount() const { return libraries_.size(); }
177
178 private:
179  struct LibraryKey {
180    LibraryKey(NSString* source, MTLCompileOptions* options)
181        : source_(source, base::scoped_policy::RETAIN),
182          options_(options, base::scoped_policy::RETAIN) {}
183    LibraryKey(const LibraryKey& other) = default;
184    LibraryKey& operator=(const LibraryKey& other) = default;
185    ~LibraryKey() = default;
186
187    bool operator<(const LibraryKey& other) const {
188      switch ([source_ compare:other.source_]) {
189        case NSOrderedAscending:
190          return true;
191        case NSOrderedDescending:
192          return false;
193        case NSOrderedSame:
194          break;
195      }
196#define COMPARE(x)                       \
197  if ([options_ x] < [other.options_ x]) \
198    return true;                         \
199  if ([options_ x] > [other.options_ x]) \
200    return false;
201      COMPARE(fastMathEnabled);
202      COMPARE(languageVersion);
203#undef COMPARE
204      // Skia doesn't set any preprocessor macros, and defining an order on two
205      // NSDictionaries is a lot of code, so just assert that there are no
206      // macros. Should this alleviate https://crbug.com/974219, then a more
207      // robust cache should be implemented.
208      DCHECK_EQ([[options_ preprocessorMacros] count], 0u);
209      return false;
210    }
211
212   private:
213    base::scoped_nsobject<NSString> source_;
214    base::scoped_nsobject<MTLCompileOptions> options_;
215  };
216  struct LibraryData {
217    LibraryData(id<MTLLibrary> library_, NSError* error_)
218        : library(library_, base::scoped_policy::RETAIN),
219          error(error_, base::scoped_policy::RETAIN) {}
220    LibraryData(const LibraryData& other) = default;
221    LibraryData& operator=(const LibraryData& other) = default;
222    ~LibraryData() = default;
223
224    base::scoped_nsprotocol<id<MTLLibrary>> library;
225    base::scoped_nsobject<NSError> error;
226  };
227
228  std::map<LibraryKey, LibraryData> libraries_;
229  DISALLOW_COPY_AND_ASSIGN(MTLLibraryCache);
230};
231
232// Disable protocol warnings and property synthesis warnings. Any unimplemented
233// methods/properties in the MTLDevice protocol will be handled by the
234// -forwardInvocation: method.
235#pragma clang diagnostic push
236#pragma clang diagnostic ignored "-Wprotocol"
237#pragma clang diagnostic ignored "-Wobjc-protocol-property-synthesis"
238
239@implementation MTLDeviceProxy
240- (id)initWithDevice:(id<MTLDevice>)device {
241  if (self = [super init]) {
242    _device.reset(device, base::scoped_policy::RETAIN);
243    _libraryCache = std::make_unique<MTLLibraryCache>();
244  }
245  return self;
246}
247
248- (void)setProgressReporter:(gl::ProgressReporter*)progressReporter {
249  _progressReporter = progressReporter;
250}
251
252- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
253  // Technically, _device is of protocol MTLDevice which inherits from protocol
254  // NSObject, and protocol NSObject does not have -methodSignatureForSelector:.
255  // Assume that the implementing class derives from NSObject.
256  return [base::mac::ObjCCastStrict<NSObject>(_device)
257      methodSignatureForSelector:selector];
258}
259
260- (void)forwardInvocation:(NSInvocation*)invocation {
261  // The number of methods on MTLDevice is finite and small, so this unbounded
262  // cache is fine. std::map does not move elements on additions to the map, so
263  // the requirement that strings passed to TRACE_EVENT0 don't move is
264  // fulfilled.
265  static base::NoDestructor<std::map<SEL, std::string>> invocationNames;
266  auto& invocationName = (*invocationNames)[invocation.selector];
267  if (invocationName.empty()) {
268    invocationName =
269        base::StringPrintf("-[MTLDevice %s]", sel_getName(invocation.selector));
270  }
271
272  TRACE_EVENT0("gpu", invocationName.c_str());
273  gl::ScopedProgressReporter scoped_reporter(_progressReporter);
274  [invocation invokeWithTarget:_device.get()];
275}
276
277- (nullable id<MTLLibrary>)
278    newLibraryWithSource:(NSString*)source
279                 options:(nullable MTLCompileOptions*)options
280                   error:(__autoreleasing NSError**)error {
281  TRACE_EVENT0("gpu", "-[MTLDevice newLibraryWithSource:options:error:]");
282
283  // Capture the shader's source in a crash key in case newLibraryWithSource
284  // hangs.
285  // https://crbug.com/974219
286  static crash_reporter::CrashKeyString<kShaderCrashDumpLength> shaderKey(
287      "MTLShaderSource");
288  std::string sourceAsSysString = base::SysNSStringToUTF8(source);
289  if (sourceAsSysString.size() > kShaderCrashDumpLength)
290    DLOG(WARNING) << "Truncating shader in crash log.";
291
292  shaderKey.Set(sourceAsSysString);
293  static crash_reporter::CrashKeyString<16> newLibraryCountKey(
294      "MTLNewLibraryCount");
295  newLibraryCountKey.Set(base::NumberToString(_libraryCache->CacheMissCount()));
296
297  id<MTLLibrary> library = _libraryCache->NewLibraryWithSource(
298      _device, source, options, error, _progressReporter);
299  shaderKey.Clear();
300  newLibraryCountKey.Clear();
301
302  // Shaders from Skia will have either a vertexMain or fragmentMain function.
303  // Save the source and a weak pointer to the function, so we can capture
304  // the shader source in -newRenderPipelineStateWithDescriptor (see further
305  // remarks in that function).
306  base::scoped_nsprotocol<id<MTLFunction>> vertexFunction(
307      [library newFunctionWithName:@"vertexMain"]);
308  if (vertexFunction) {
309    _vertexSourceFunction = vertexFunction;
310    _vertexSource = sourceAsSysString;
311  }
312  base::scoped_nsprotocol<id<MTLFunction>> fragmentFunction(
313      [library newFunctionWithName:@"fragmentMain"]);
314  if (fragmentFunction) {
315    _fragmentSourceFunction = fragmentFunction;
316    _fragmentSource = sourceAsSysString;
317  }
318
319  return library;
320}
321
322- (nullable id<MTLRenderPipelineState>)
323    newRenderPipelineStateWithDescriptor:
324        (MTLRenderPipelineDescriptor*)descriptor
325                                   error:(__autoreleasing NSError**)error {
326  TRACE_EVENT0("gpu",
327               "-[MTLDevice newRenderPipelineStateWithDescriptor:error:]");
328  // Capture the vertex and shader source being used. Skia's use pattern is to
329  // compile two MTLLibraries before creating a MTLRenderPipelineState -- one
330  // with vertexMain and the other with fragmentMain. The two immediately
331  // previous -newLibraryWithSource calls should have saved the sources for
332  // these two functions.
333  // https://crbug.com/974219
334  static crash_reporter::CrashKeyString<kShaderCrashDumpLength> vertexShaderKey(
335      "MTLVertexSource");
336  if (_vertexSourceFunction == [descriptor vertexFunction])
337    vertexShaderKey.Set(_vertexSource);
338  else
339    DLOG(WARNING) << "Failed to capture vertex shader.";
340  static crash_reporter::CrashKeyString<kShaderCrashDumpLength>
341      fragmentShaderKey("MTLFragmentSource");
342  if (_fragmentSourceFunction == [descriptor fragmentFunction])
343    fragmentShaderKey.Set(_fragmentSource);
344  else
345    DLOG(WARNING) << "Failed to capture fragment shader.";
346  static crash_reporter::CrashKeyString<16> newLibraryCountKey(
347      "MTLNewLibraryCount");
348  newLibraryCountKey.Set(base::NumberToString(_libraryCache->CacheMissCount()));
349
350  SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewRenderPipelineStateTime");
351  id<MTLRenderPipelineState> pipelineState = NewRenderPipelineStateWithRetry(
352      _device, descriptor, error, _progressReporter);
353
354  vertexShaderKey.Clear();
355  fragmentShaderKey.Clear();
356  newLibraryCountKey.Clear();
357  return pipelineState;
358}
359
360@end
361
362#pragma clang diagnostic pop
363