Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/common/cocoa_tools.mm
7528 views
// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team, 2019-2024 Connor McLaughlin <[email protected]>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0

#include "cocoa_tools.h"
#include "assert.h"
#include "error.h"
#include "log.h"
#include "small_string.h"

#include "fmt/format.h"

#include <AppKit/AppKit.h>
#include <Cocoa/Cocoa.h>
#include <QuartzCore/QuartzCore.h>
#include <cinttypes>
#include <dlfcn.h>
#include <mach/mach_time.h>
#include <vector>

#if __has_feature(objc_arc)
#error ARC should not be enabled.
#endif

NSString* CocoaTools::StringViewToNSString(std::string_view str)
{
  if (str.empty())
    return nil;

  return [[[NSString alloc] initWithBytes:str.data()
                                   length:static_cast<NSUInteger>(str.length())
                                 encoding:NSUTF8StringEncoding] autorelease];
}

std::string CocoaTools::NSErrorToString(NSError* error)
{
  return fmt::format("{}: {}", static_cast<u32>(error.code), [error.description UTF8String]);
}

void CocoaTools::NSErrorToErrorObject(Error* errptr, std::string_view message, NSError* error)
{
  Error::SetStringFmt(errptr, "{}NSError Code {}: {}", message, static_cast<u32>(error.code),
                      [error.description UTF8String]);
}

bool CocoaTools::MoveFile(const char* source, const char* destination, Error* error)
{
  @autoreleasepool
  {
    NSError* nserror;
    const BOOL result = [[NSFileManager defaultManager] moveItemAtPath:[NSString stringWithUTF8String:source]
                                                                toPath:[NSString stringWithUTF8String:destination]
                                                                 error:&nserror];
    if (!result)
    {
      Error::SetString(error, NSErrorToString(nserror));
      return false;
    }

    return true;
  }
}

// Used for present timing.
static const struct mach_timebase_info s_timebase_info = []() {
  struct mach_timebase_info val;
  const kern_return_t res = mach_timebase_info(&val);
  Assert(res == KERN_SUCCESS);
  return val;
}();

u64 CocoaTools::ConvertMachTimeBaseToNanoseconds(u64 time)
{
  return ((time * s_timebase_info.numer) / s_timebase_info.denom);
}

u64 CocoaTools::ConvertNanosecondsToMachTimeBase(u64 time)
{
  return ((time * s_timebase_info.denom) / s_timebase_info.numer);
}

std::optional<std::string> CocoaTools::GetBundlePath()
{
  std::optional<std::string> ret;
  @autoreleasepool
  {
    NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
    if (url)
      ret = std::string([url fileSystemRepresentation]);
  }
  return ret;
}

std::optional<std::string> CocoaTools::GetNonTranslocatedBundlePath()
{
  // See https://objective-see.com/blog/blog_0x15.html
  std::optional<std::string> ret;
  @autoreleasepool
  {
    NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
    if (!url)
      return ret;

    if (void* handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY))
    {
      auto IsTranslocatedURL =
        reinterpret_cast<Boolean (*)(CFURLRef path, bool* isTranslocated, CFErrorRef* __nullable error)>(
          dlsym(handle, "SecTranslocateIsTranslocatedURL"));
      auto CreateOriginalPathForURL =
        reinterpret_cast<CFURLRef __nullable (*)(CFURLRef translocatedPath, CFErrorRef* __nullable error)>(
          dlsym(handle, "SecTranslocateCreateOriginalPathForURL"));
      bool is_translocated = false;
      if (IsTranslocatedURL)
        IsTranslocatedURL((__bridge CFURLRef)url, &is_translocated, nullptr);
      if (is_translocated)
      {
        if (CFURLRef actual = CreateOriginalPathForURL((__bridge CFURLRef)url, nullptr))
          url = (__bridge NSURL*)actual;
      }
      dlclose(handle);
    }

    ret = std::string([url fileSystemRepresentation]);
  }

  return ret;
}

bool CocoaTools::DelayedLaunch(std::string_view file, std::span<const std::string_view> args)
{
  @autoreleasepool
  {
    const int pid = [[NSProcessInfo processInfo] processIdentifier];

    // Hopefully we're not too large here...
    std::string task_args =
      fmt::format("while /bin/ps -p {} > /dev/null; do /bin/sleep 0.1; done; exec /usr/bin/open \"{}\"", pid, file);
    if (!args.empty())
    {
      task_args += " --args";
      for (const std::string_view& arg : args)
      {
        task_args += " \"";
        task_args += arg;
        task_args += "\"";
      }
    }

    NSTask* task = [NSTask new];
    [task setExecutableURL:[NSURL fileURLWithPath:@"/bin/sh"]];
    [task setArguments:@[ @"-c", [NSString stringWithUTF8String:task_args.c_str()] ]];
    return [task launchAndReturnError:nil];
  }
}

std::optional<std::pair<int, int>> CocoaTools::GetViewSizeInPixels(const void* view)
{
  std::optional<std::pair<int, int>> ret;
  if (view)
  {
    NSView* nsview = (__bridge NSView*)view;
    const NSSize size = [nsview convertSizeToBacking:nsview.frame.size];
    ret = std::make_pair(static_cast<int>(size.width), static_cast<int>(size.height));
  }

  return ret;
}

std::optional<double> CocoaTools::GetViewRealScalingFactor(const void* view)
{
  if (!view)
    return std::nullopt;

  NSView* const nsview = (__bridge NSView*)view;
  NSWindow* const nswindow = nsview.window;
  if (nswindow == nil)
    return std::nullopt;

  NSScreen* const nsscreen = nswindow.screen;
  if (nsscreen == nil)
    return std::nullopt;

  const u32 did = [[nsscreen.deviceDescription valueForKey:@"NSScreenNumber"] unsignedIntValue];
  const NSArray* all_modes = (__bridge NSArray*)CGDisplayCopyAllDisplayModes(did, nil);
  if (all_modes == nil)
  {
    GENERIC_LOG(Log::Channel::WindowInfo, Log::Level::Dev, Log::Color::Default,
                "GetViewRealScalingFactor(): CGDisplayCopyAllDisplayModes() failed");
    return std::nullopt;
  }

  u32 max_width = 0;
  for (NSUInteger i = 0; i < all_modes.count; i++)
    max_width = std::max(max_width, static_cast<u32>(CGDisplayModeGetPixelWidth((CGDisplayModeRef)all_modes[i])));
  CFRelease(all_modes);
  if (max_width == 0)
  {
    GENERIC_LOG(Log::Channel::WindowInfo, Log::Level::Dev, Log::Color::Default,
                "GetViewRealScalingFactor(): Max width is zero");
    return std::nullopt;
  }

  // Sanity check: Scale should not be less than 100%, and cannot be more than 200%.
  const CGFloat frame_width = nsscreen.frame.size.width;
  const CGFloat scale = static_cast<CGFloat>(max_width) / frame_width;
  GENERIC_LOG(Log::Channel::WindowInfo, Log::Level::Dev, Log::Color::Default,
              "GetViewRealScalingFactor(): MaxWidth={}, FrameWidth={}, Scale={}", max_width, frame_width, scale);
  if (scale < 1.0f)
    return std::nullopt;

  return static_cast<double>(scale);
}

std::optional<float> CocoaTools::GetViewRefreshRate(const void* view, Error* error)
{
  if (!view)
    return std::nullopt;

  if (![NSThread isMainThread])
  {
    std::optional<float> ret;
    dispatch_sync(dispatch_get_main_queue(), [&ret, view, error] { ret = GetViewRefreshRate(view, error); });
    return ret;
  }

  std::optional<float> ret;
  NSView* const nsview = (__bridge NSView*)view;
  const u32 did = [[[[[nsview window] screen] deviceDescription] valueForKey:@"NSScreenNumber"] unsignedIntValue];
  if (CGDisplayModeRef mode = CGDisplayCopyDisplayMode(did))
  {
    ret = CGDisplayModeGetRefreshRate(mode);
    if (ret.value() <= 0.0f)
    {
      ret.reset();

      // Ignore deprecration warnings here. The new APIs don't seem to have something that matches the semantics.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

      CVDisplayLinkRef link = nullptr;
      if (CVDisplayLinkCreateWithCGDisplay(did, &link) == 0)
      {
        const CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link);
        if (!(time.flags & kCVTimeIsIndefinite) && time.timeValue != 0)
        {
          ret = static_cast<float>(static_cast<double>(time.timeScale) / static_cast<double>(time.timeValue));
        }
        else
        {
          Error::SetStringFmt(error, "Refresh period is invalid (flags=0x{:X}, timeValue={}, timeScale={})",
                              static_cast<u32>(time.flags), time.timeValue, time.timeScale);
        }
      }
      else
      {
        Error::SetStringView(error, "CVDisplayLinkCreateWithCGDisplay() failed");
      }
#pragma clang diagnostic pop
    }
    CGDisplayModeRelease(mode);
  }
  else
  {
    Error::SetStringView(error, "CGDisplayCopyDisplayMode() failed");
  }

  return ret;
}

void* CocoaTools::CreateMetalLayer(void* view, Error* error)
{
  // Punt off to main thread if we're not calling from it already.
  if (![NSThread isMainThread])
  {
    void* ret;
    dispatch_sync(dispatch_get_main_queue(), [&ret, view, error]() { ret = CreateMetalLayer(view, error); });
    return ret;
  }

  CAMetalLayer* layer = [[CAMetalLayer layer] retain];
  if (layer == nil)
  {
    Error::SetStringView(error, "Failed to create CAMetalLayer");
    return nullptr;
  }

  NSView* nsview = (__bridge NSView*)view;
  [nsview setWantsLayer:TRUE];
  [nsview setLayer:layer];
  [layer setContentsScale:[[[nsview window] screen] backingScaleFactor]];

  return layer;
}

void CocoaTools::DestroyMetalLayer(void* view, void* layer)
{
  // Punt off to main thread if we're not calling from it already.
  if (![NSThread isMainThread])
  {
    dispatch_sync(dispatch_get_main_queue(), [view, layer]() { DestroyMetalLayer(view, layer); });
    return;
  }

  NSView* nsview = (__bridge NSView*)view;
  CAMetalLayer* clayer = (CAMetalLayer*)layer;
  [nsview setLayer:nil];
  [nsview setWantsLayer:NO];
  [clayer release];
}

void Y_OnAssertFailed(const char* szMessage, const char* szFunction, const char* szFile, unsigned uLine)
{
  if (![NSThread isMainThread])
  {
    dispatch_sync(dispatch_get_main_queue(),
                  [szMessage, szFunction, szFile, uLine]() { Y_OnAssertFailed(szMessage, szFunction, szFile, uLine); });
    return;
  }

  char szMsg[512];
  std::snprintf(szMsg, sizeof(szMsg), "%s in function %s (%s:%u)\n", szMessage, szFunction, szFile, uLine);
  std::fputs(szMsg, stderr);
  std::fflush(stderr);

  @autoreleasepool
  {
    NSAlert* alert = [[[NSAlert alloc] init] autorelease];
    [alert setMessageText:@"Assertion Failed"];

    NSString* text = [NSString stringWithFormat:@"%s in function %s (%s:%u)\nPress Abort to exit, Break to break to "
                                                @"debugger, or Ignore to attempt to continue.",
                                                szMessage, szFunction, szFile, uLine];
    [alert setInformativeText:text];
    [alert setAlertStyle:NSAlertStyleCritical];
    [alert addButtonWithTitle:@"Abort"];
    [alert addButtonWithTitle:@"Break"];
    [alert addButtonWithTitle:@"Ignore"];

    const NSModalResponse response = [alert runModal];
    if (response == NSAlertFirstButtonReturn)
      std::abort();
    else if (response == NSAlertSecondButtonReturn)
      __builtin_debugtrap();
  }
}

[[noreturn]] void Y_OnPanicReached(const char* szMessage, const char* szFunction, const char* szFile, unsigned uLine)
{
  if (![NSThread isMainThread])
  {
    dispatch_sync(dispatch_get_main_queue(),
                  [szMessage, szFunction, szFile, uLine]() { Y_OnAssertFailed(szMessage, szFunction, szFile, uLine); });
  }
  else
  {
    char szMsg[512];
    std::snprintf(szMsg, sizeof(szMsg), "%s in function %s (%s:%u)\n", szMessage, szFunction, szFile, uLine);

    @autoreleasepool
    {
      NSAlert* alert = [[[NSAlert alloc] init] autorelease];
      [alert setMessageText:@"Critical Error"];

      NSString* text =
        [NSString stringWithFormat:@"%s in function %s (%s:%u)\nDo you want to attempt to break into a debugger?",
                                   szMessage, szFunction, szFile, uLine];
      [alert setInformativeText:text];
      [alert setAlertStyle:NSAlertStyleCritical];
      [alert addButtonWithTitle:@"Abort"];
      [alert addButtonWithTitle:@"Break"];

      const NSModalResponse response = [alert runModal];
      if (response == NSAlertSecondButtonReturn)
        __builtin_debugtrap();
    }
  }

  std::abort();
}