Add CAGradientLayer, Allow Layer Flattening for proper masking and opacity compositing#408
Add CAGradientLayer, Allow Layer Flattening for proper masking and opacity compositing#408ephemer wants to merge 6 commits intomultipleVideosfrom
Conversation
…dd CAGradientLayer
| contentsScale = 1.0 // this doesn't work on init because we set contentsScale in UIView.init afterwards | ||
| contents = VideoTexture(width: width, height: height, format: GPU_FORMAT_RGBA) | ||
| } | ||
|
|
||
| // Swap R and B values to get RGBA pixels instead of BGRA: | ||
| let bufferSize = CVPixelBufferGetDataSize(pixelBuffer) | ||
| for i in stride(from: 0, to: bufferSize, by: 16) { | ||
| swap(&pixelBytes[i], &pixelBytes[i + 2]) | ||
| swap(&pixelBytes[i+4], &pixelBytes[i + 6]) | ||
| swap(&pixelBytes[i+8], &pixelBytes[i + 10]) | ||
| swap(&pixelBytes[i+12], &pixelBytes[i + 14]) | ||
| contents = VideoTexture(width: width, height: height, format: GPU_FORMAT_BGRA) |
There was a problem hiding this comment.
this change dramatically improves video performance on macOS – esp. in debug builds
| width: (layer.contents.map { CGFloat($0.width) } ?? layer.bounds.width) / layer.contentsScale, | ||
| height: (layer.contents.map { CGFloat($0.height) } ?? layer.bounds.height) / layer.contentsScale |
There was a problem hiding this comment.
turns out this is actually unused but it prevents the crash mentioned on line 22 (I could remove that comment now I guess)
| @MainActor | ||
| final func sdlRender(parentAbsoluteOpacity: Float = 1) { | ||
| guard let renderer = UIScreen.main else { return } | ||
| final func sdlRender(parentAbsoluteOpacity: Float = 1, renderTarget: RenderTarget) { |
There was a problem hiding this comment.
the concept of the RenderTarget being able to change is the main innovation in this PR: to begin with we start rendering to the window (which is the base-level RenderTarget). If we encounter a non-trivial CALayer with a mask/opacity, we render its entire subtree to a separate RenderTarget (which is literally an image in memory), and then we copy that pre-rendered image onto the window with the correct mask and opacity.
| height = Int(rawPointer.pointee.h) | ||
| } | ||
|
|
||
| public func savePNG(filename: String) { |
There was a problem hiding this comment.
debugging API – I should probably prefix this with an underscore
| let newLineCharacterCode = 10 | ||
| if characterCode != spaceCharacterCode, characterCode != newLineCharacterCode, glyph.maxx - glyph.minx <= 0 { | ||
| assertionFailure("Glyph \(characterCode) ('\(Character(UnicodeScalar(characterCode)!))') has no width") | ||
| // assertionFailure("Glyph \(characterCode) ('\(Character(UnicodeScalar(characterCode)!))') has no width") |
There was a problem hiding this comment.
this change was unintentional – will revert it
| extension RenderTarget { | ||
| func blit( | ||
| _ image: CGImage, | ||
| anchorPoint: CGPoint, | ||
| contentsScale: CGFloat, | ||
| contentsGravity: ContentsGravityTransformation, | ||
| opacity: Float | ||
| ) throws { | ||
| GPU_SetAnchor(image.rawPointer, Float(anchorPoint.x), Float(anchorPoint.y)) | ||
| GPU_SetRGBA(image.rawPointer, 255, 255, 255, opacity.normalisedToUInt8()) | ||
|
|
||
| GPU_BlitTransform( | ||
| image.rawPointer, | ||
| nil, | ||
| self.rawPointer, | ||
| Float(contentsGravity.offset.x), | ||
| Float(contentsGravity.offset.y), | ||
| 0, // rotation in degrees | ||
| Float(contentsGravity.scale.width / contentsScale), | ||
| Float(contentsGravity.scale.height / contentsScale) | ||
| ) | ||
|
|
||
| try throwOnErrors(ofType: [GPU_ERROR_USER_ERROR]) | ||
| } | ||
|
|
||
| func blit(renderTarget: RenderTarget, opacity: Float) throws { | ||
| GPU_SetAnchor(renderTarget.rawPointer.pointee.image, 0, 0) | ||
| GPU_SetRGBA(renderTarget.rawPointer.pointee.image, 255, 255, 255, opacity.normalisedToUInt8()) | ||
|
|
||
| GPU_BlitTransform( | ||
| renderTarget.rawPointer.pointee.image, | ||
| nil, | ||
| self.rawPointer, | ||
| 0, // offset | ||
| 0, // offset | ||
| 0, // rotation in degrees | ||
| Float(1 / scale), | ||
| Float(1 / scale) | ||
| ) | ||
|
|
||
| try throwOnErrors(ofType: [GPU_ERROR_USER_ERROR]) | ||
| } | ||
|
|
||
| func setShapeBlending(_ newValue: Bool) { | ||
| GPU_SetShapeBlending(newValue) | ||
| } | ||
|
|
||
| func setShapeBlendMode(_ newValue: GPU_BlendPresetEnum) { | ||
| GPU_SetShapeBlendMode(newValue) | ||
| } | ||
|
|
||
| func clear() { | ||
| GPU_Clear(rawPointer) | ||
| } | ||
|
|
||
| func fill(_ rect: CGRect, with color: UIColor, cornerRadius: CGFloat) { | ||
| if cornerRadius >= 1 { | ||
| GPU_RectangleRoundFilled(rawPointer, rect.gpuRect(scale: scale), cornerRadius: Float(cornerRadius), color: color.sdlColor) | ||
| } else { | ||
| GPU_RectangleFilled(rawPointer, rect.gpuRect(scale: scale), color: color.sdlColor) | ||
| } | ||
| } | ||
|
|
||
| func outline(_ rect: CGRect, lineColor: UIColor, lineThickness: CGFloat) { | ||
| // we want to render the outline 'inside' the rect rather | ||
| // than exceeding the bounds when lineThickness is bigger than 1 | ||
| let offset = lineThickness / 2 | ||
| let scaledGpuRect = CGRect( | ||
| x: rect.origin.x + offset, | ||
| y: rect.origin.y + offset, | ||
| width: rect.size.width - offset, | ||
| height: rect.size.height - offset | ||
| ).gpuRect(scale: scale) | ||
|
|
||
| GPU_SetLineThickness(Float(lineThickness)) | ||
| GPU_Rectangle(rawPointer, scaledGpuRect, color: lineColor.sdlColor) | ||
| } | ||
|
|
||
| func outline(_ rect: CGRect, lineColor: UIColor, lineThickness: CGFloat, cornerRadius: CGFloat) { | ||
| if cornerRadius > 1 { | ||
| // we want to render the outline 'inside' the rect rather | ||
| // than exceeding the bounds when lineThickness is bigger than 1 | ||
| let offset = lineThickness / 2 | ||
| let scaledGpuRect = CGRect( | ||
| x: rect.origin.x + offset, | ||
| y: rect.origin.y + offset, | ||
| width: rect.size.width - offset, | ||
| height: rect.size.height - offset | ||
| ).gpuRect(scale: scale) | ||
|
|
||
| GPU_SetLineThickness(Float(lineThickness)) | ||
| GPU_RectangleRound(rawPointer, scaledGpuRect, cornerRadius: Float(cornerRadius), color: lineColor.sdlColor) | ||
| } else { | ||
| outline(rect, lineColor: lineColor, lineThickness: lineThickness) | ||
| } | ||
| } | ||
|
|
||
| func flip() throws { | ||
| GPU_Flip(rawPointer) | ||
| try throwOnErrors(ofType: [GPU_ERROR_USER_ERROR, GPU_ERROR_BACKEND_ERROR]) | ||
| } | ||
|
|
||
| func setVirtualResolution(w: UInt16, h: UInt16) { | ||
| GPU_SetVirtualResolution(rawPointer, w, h) | ||
| } | ||
| } |
There was a problem hiding this comment.
almost everything here is copied directly from UIScreen+render.swift -> previously UIScreen was our only render target, but now we can have various targets (see comments and code above)
| // | ||
| // Mask.swift | ||
| // UIKit | ||
| // | ||
| // Created by Geordie Jay on 25.10.17. | ||
| // Copyright © 2017 flowkey. All rights reserved. | ||
| // |
There was a problem hiding this comment.
should probably remove this..
| private var startPoint: UniformVariable! | ||
| private var endPoint: UniformVariable! | ||
|
|
||
| // we only need one MaskShaderProgram, which we can / should treat as a singleton |
There was a problem hiding this comment.
This entire file is unused in this PR – it's a GPU implementation for CAGradientLayer but integrating it would require some more work, and we don't need the flexibility or the performance of using the GPU for our needs right now
| if visibleLayer.mask?.needsDisplay() == true { | ||
| visibleLayer.mask?.display() | ||
| visibleLayer.mask?._needsDisplay = false | ||
| } |
There was a problem hiding this comment.
this can be removed because we (also) display() in the sdlRender function
Type of change: Feature
Motivation (current vs expected behavior)