Skip to content

Conversation

@ephemer
Copy link
Member

@ephemer ephemer commented Dec 12, 2025

Type of change: Feature

Motivation (current vs expected behavior)

  • Add a simple CPU-only (for now) CAGradientLayer and the beginnings of a GPU implementation.
  • Add layer flattening
    • With this, non-trivial layers with non-zero opacity and/or a mask are rendered as a separate render tree and then composited onto the window (or into each other)
    • This enables proper masking, e.g. via a gradient layer (via alpha) or using non-trivial shapes

@ephemer ephemer requested a review from michaelknoch December 12, 2025 15:49
@ephemer ephemer changed the base branch from master to multipleVideos December 12, 2025 15:49
Comment on lines 110 to +111
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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change dramatically improves video performance on macOS – esp. in debug builds

Comment on lines +25 to +26
width: (layer.contents.map { CGFloat($0.width) } ?? layer.bounds.width) / layer.contentsScale,
height: (layer.contents.map { CGFloat($0.height) } ?? layer.bounds.height) / layer.contentsScale
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change was unintentional – will revert it

Comment on lines +47 to +152
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)
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +1 to +7
//
// Mask.swift
// UIKit
//
// Created by Geordie Jay on 25.10.17.
// Copyright © 2017 flowkey. All rights reserved.
//
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +28 to +31
if visibleLayer.mask?.needsDisplay() == true {
visibleLayer.mask?.display()
visibleLayer.mask?._needsDisplay = false
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be removed because we (also) display() in the sdlRender function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants