[Metal] 9072 by 12096 iosurface is too large for GPU

I take a picture using the iPhone's camera. The taken resolution is 3024.0 x 4032. I then have to apply a watermark to this image. After a bunch of trial and error, the method I decided to use was taking a snapshot of a watermark UIView, and drawing that over the image, like so:

// Create the watermarked photo.
let result: UIImage=UIGraphicsImageRenderer(size: image.size).image(actions: { _ in
  image.draw(in: .init(origin: .zero, size: image.size))
  let watermark: Watermark = .init(
    size: image.size,
    scaleFactor: image.size.smallest / self.frame.size.smallest
  )
  watermark.drawHierarchy(in: .init(origin: .zero, size: image.size), afterScreenUpdates: true)
})

Then with the final image — because the client wanted it to have a filename as well when viewed from within the Photos app and exported from it, and also with much trial and error — I save it to a file in a temporary directory. I then save it to the user's Photo library using that file. The difference as compared to saving the image directly vs saving it from the file is that when saved from the file the filename is used as the filename within the Photos app; and in the other case it's just a default photo name generated by Apple.

The problem is that in the image saving code I'm getting the following error:

[Metal] 9072 by 12096 iosurface is too large for GPU

And when I view the saved photo it's basically just a completely black image. This problem only started when I changed the AVCaptureSession preset to .photo. Before then there was no errors.

Now, the worst problem is that the app just completely crashes on drawing of the watermark view in the first place. When using .photo the resolution is significantly higher, so the image size is larger, so the watermark size has to be commensurately larger as well. iOS appears to be okay with the size of the watermark UIView. However, when I try to draw it over the image the app crashes with this message from Xcode:

So there's that problem. But I figured that could be resolved by taking a more manual approach to the drawing of the watermark then using a UIView snapshot. So it's not the most pressing problem. What is, is that even after the drawing code is commented out, I still get the iosurface is too large error.

Here's the code that saves the image to the file and then to the Photos library:

extension UIImage {

  /// Save us with the given name to the user's photo album.
  /// - Parameters:
  ///  - filename: The filename to be used for the saved photo. Behavior is undefined if the filename contain characters other than what is represented by this regular expression [A-Za-z0-9-_]. A decimal point for the file extension is permitted.
  ///  - location: A GPS location to save with the photo.
  fileprivate func save(_ filename: String, _ location: Optional<Coordinates>) throws {
     
    // Create a path to a temporary directory. Adding filenames to the Photos app form of images is accomplished by first creating an image file on the file system, saving the photo using the URL to that file, and then deleting that file on the file system.
    //   A documented way of adding filenames to photos saved to Photos was never found.
    // Furthermore, we save everything to a `tmp` directory as if we just tried deleting individual photos after they were saved, and the deletion failed, it would be a little more tricky setting up logic to ensure that the undeleted files are eventually
    // cleaned up. But by using a `tmp` directory, we can save all temporary photos to it, and delete the entire directory following each taken picture.
    guard
      let tmpUrl: URL=try {
        guard let documentsDirUrl=NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else {
          throw GeneralError("Failed to create URL to documents directory.")
        }
        let url: Optional<URL> = .init(string: documentsDirUrl + "/tmp/")
        return url
      }()
    else {
      throw GeneralError("Failed to create URL to temporary directory.")
    }
     
    // A path to the image file.
    let filePath: String=try {
       
      // Reduce the likelihood of photos taken in quick succession from overwriting each other.
      let collisionResistantPath: String="\(tmpUrl.path(percentEncoded: false))\(UUID())/"
       
      // Make sure all directories required by the path exist before trying to write to it.
      try FileManager.default.createDirectory(atPath: collisionResistantPath, withIntermediateDirectories: true, attributes: nil)
       
      // Done.
      return collisionResistantPath + filename
    }()

    // Create `CFURL` analogue of file path.
    guard let cfPath: CFURL=CFURLCreateWithFileSystemPath(nil, filePath as CFString, CFURLPathStyle.cfurlposixPathStyle, false) else {
      throw GeneralError("Failed to create `CFURL` analogue of file path.")
    }
     
    // Create image destination object.
    //
    // You can change your exif type here.
    //   This is a note from original author. Not quite exactly sure what they mean by it. Link in method documentation can be used to refer back to the original context.
    guard let destination=CGImageDestinationCreateWithURL(cfPath, UTType.jpeg.identifier as CFString, 1, nil) else {
      throw GeneralError("Failed to create `CGImageDestination` from file url.")
    }
     
    // Metadata properties.
    let properties: CFDictionary={
       
      // Place your metadata here.
      // Keep in mind that metadata follows a standard. You can not use custom property names here.
      let tiffProperties: Dictionary<String, Any>=[:]
       
      return [
        kCGImagePropertyExifDictionary as String: tiffProperties
      ] as CFDictionary
    }()
     
    // Create image file.
    guard let cgImage=self.cgImage else {
      throw GeneralError("Failed to retrieve `CGImage` analogue of `UIImage`.")
    }
    CGImageDestinationAddImage(destination, cgImage, properties)
    CGImageDestinationFinalize(destination)
       
    // Save to the photo library.
    PHPhotoLibrary.shared().performChanges({
      guard let creationRequest: PHAssetChangeRequest = .creationRequestForAssetFromImage(atFileURL: URL(fileURLWithPath: filePath)) else {
        return
      }

      // Add metadata to the photo.
      creationRequest.creationDate = .init()
      if let location=location {
        creationRequest.location = .init(latitude: location.latitude, longitude: location.longitude)
      }
    }, completionHandler: { _, _ in
      try? FileManager.default.removeItem(atPath: tmpUrl.absoluteString)
    })
  }
}

If anyone can provide some insight as to what's causing the iosurface is too large error and what can be done to resolve it, that'd be awesome.

Replies

Hello,

Could you add some logging to your code that prints the size of the image and the size of the watermark? I suspect that one or the other is very large, and you are calling something that is using the GPU to render, but you have surfaces that are too large for the GPU on the device you are testing with.

Also, as an aside, there are likely more efficient ways to create the watermarked image (I recommend CoreImage), of course you know your app best so perhaps you have a good reason to do it with drawHierarchy.

  • The size of the image and the watermark UIView is the same as the resolution of the exported image: 3024 x 4032. I have no idea where the 9072 x 12096 comes from in the error. It's obviously the size/resolution times three, but I have no idea why it's doing that. If I remove the watermark logic, then my code does no multiplication at all — the multiplication takes place in the watermark to ensure the text is scaled properly with the size of the image (doesn't appear super small).

Add a Comment

The reason I've taken the the approach I have for the watermark is because the watermark isn't just an image or simple string, but somewhat complex. It's basically two black translucent bars — one on the top and one on the bottom. In the bar on the top there is a description entered by the user, and beneath the description is a separate, non-constant string; both one line max.

In the bottom bar, it's pretty much the same except the top string are GPS coordinates and the bottom one is a timestamp. Furthermore, the watermark contains the app's logo. Using the approach I have makes scaling convenient as I can use AutoLayout to ensure that things are sized appropriately for the resolution of the captured photo.

Did you fix it finally??

  • it's been a while, but i don't think i did. i ended up compromising on image quality and not using .photo for preset. IIRC, that reduced the overal resolution, didn't result in complaints by the system about the size of the watermark, and was good enough.

Add a Comment