Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// swift-tools-version:5.7
// swift-tools-version:6.0
import PackageDescription

let package = Package(
name: "UIKit",
platforms: [.macOS(.v10_15)],
platforms: [.macOS(.v13)],
products: [
.library(name: "UIKit", targets: ["UIKit"])
],
dependencies: [
.package(path: "./swift-jni"),
.package(url: "https://github.com/SwiftAndroid/swift-jni", branch: "devel"),
.package(path: "./SDL"),
],
targets: [
Expand All @@ -23,5 +23,6 @@ let package = Package(
exclude: ["Mac-Info.plist"]
),
.target(name: "UIKit_C_API", path: "UIKit_C_API"),
]
],
swiftLanguageModes: [.v5]
)
2 changes: 1 addition & 1 deletion SDL
22 changes: 6 additions & 16 deletions Sources/AVPlayerLayer+Mac.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//

import AVFoundation
internal import var SDL_gpu.GPU_FORMAT_RGBA
internal import var SDL_gpu.GPU_FORMAT_BGRA

public typealias AVPlayer = AVFoundation.AVPlayer

Expand Down Expand Up @@ -69,13 +69,12 @@ public final class AVPlayerLayer: CALayer {
player?.currentItem?.remove(playerOutput)

let aspectRatio = presentationSize.width / presentationSize.height
let width = round(size.width)

let width = (size.width * self.contentsScale).rounded()
let widthAlignedTo4PixelPadding = (width.remainder(dividingBy: 8) == 0) ?
width : // <-- no padding required
width + (8 - width.remainder(dividingBy: 8))
width + (8 - width.remainder(dividingBy: 8).magnitude)


playerOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
kCVPixelBufferOpenGLCompatibilityKey as String: true,
Expand Down Expand Up @@ -109,19 +108,10 @@ public final class AVPlayerLayer: CALayer {

if contents?.width != width || contents?.height != height {
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)
}

contents?.replacePixels(with: pixelBytes, bytesPerPixel: 4)
}
}
#endif
#endif // os(macOS)
172 changes: 172 additions & 0 deletions Sources/CAGradientLayer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
internal import SDL
private import SDL_gpu

open class CAGradientLayer: CALayer {
public var colors: [CGColor] = [] {
didSet {
_colors = colors.map {
SIMD4<CGFloat>(
x: CGFloat($0.redValue) / 255,
y: CGFloat($0.greenValue) / 255,
z: CGFloat($0.blueValue) / 255,
w: CGFloat($0.alphaValue) / 255
)
}
setNeedsDisplay()
}
}

private var _colors: [SIMD4<CGFloat>] = []

public var locations: [Float] = [] {
didSet { setNeedsDisplay() }
}

public var startPoint = CGPoint(x: 0.5, y: 0.0) {
didSet { setNeedsDisplay() }
}

public var endPoint = CGPoint(x: 0.5, y: 1.0) {
didSet { setNeedsDisplay() }
}

@_optimize(speed)
override open func display() {
super.display()

if bounds.width.isZero || bounds.height.isZero {
return
}

if locations.count < colors.count {
if colors.count == 1 {
backgroundColor = colors[0]
colors = []
locations = []
} else {
locations = colors.indices.map { Float($0) / Float(colors.count - 1) }
}

}

if colors.isEmpty {
contents = nil
return
}

let width: Int32
let height: Int32

if startPoint.x == endPoint.x {
width = 1
height = Int32(bounds.height * self.contentsScale)
} else if startPoint.y == endPoint.y {
width = Int32(bounds.width * self.contentsScale)
height = 1
} else {
// pessimistic case where we need to fill the entire thing
width = Int32(bounds.width * self.contentsScale)
height = Int32(bounds.height * self.contentsScale)
}

guard let surface = SDL_CreateRGBSurfaceWithFormat(
0, // surface flags are always 0 in SDL2
width,
height,
32, // bit depth
UInt32(SDL_PIXELFORMAT_RGBA32)
) else {
return
}

SDL_LockSurface(surface)

let startPoint = SIMD2(x: self.startPoint.x, y: self.startPoint.y)
let endPoint = SIMD2(x: self.endPoint.x, y: self.endPoint.y)

for y in 0 ..< Int(height) {
let pixelPosY = y * Int(surface.pointee.pitch)

for x in 0 ..< Int(width) {
let pixel = surface.pointee.pixels.assumingMemoryBound(to: UInt8.self)
.advanced(by: pixelPosY)
.advanced(by: x * Int(surface.pointee.format.pointee.BytesPerPixel))

let p = SIMD2(x: CGFloat(x) / bounds.width, y: CGFloat(y) / bounds.height)

// Direction of gradient line
let dir = endPoint - startPoint
let len2 = dot(dir, dir)

var t: CGFloat = 0.0
if len2 > 0.00001 {
// Project (p - startPoint) onto the gradient direction
t = dot(p - startPoint, dir) / len2
}

t = max(0.0, min(t, 1.0))
let color = sampleGradient(t)

let normalized = color * 255
pixel[0] = UInt8(clamping: Int(normalized.x))
pixel[1] = UInt8(clamping: Int(normalized.y))
pixel[2] = UInt8(clamping: Int(normalized.z))
pixel[3] = UInt8(clamping: Int(normalized.w))
}
}

SDL_UnlockSurface(surface)

if
let contents,
contents.width != Int(bounds.width) ||
contents.height != Int(bounds.height)
{
contents.replacePixels(
with: surface.pointee.pixels.assumingMemoryBound(to: UInt8.self),
bytesPerPixel: Int(surface.pointee.format.pointee.BytesPerPixel)
)
} else {
contents = CGImage(surface: surface)
}
}

@_optimize(speed)
func sampleGradient(_ position: CGFloat) -> SIMD4<CGFloat> {
precondition(colors.count >= 2)
precondition(locations.count >= 2)

let t = Float(max(0.0, min(position, 1.0)))

if t <= locations.first! {
return _colors.first!
} else if t >= locations.last! {
return _colors.last!
}

// Find stops [i, i+1] containing t
for i in 0 ..< (colors.count - 1) {
let loc0 = locations[i]
let loc1 = locations[i + 1]

if t >= loc0 && t <= loc1 {
let localT = (t - loc0) / (loc1 - loc0)
return mix(_colors[i], _colors[i + 1], t: CGFloat(localT))
}
}

return _colors.last!
}
}


// GLSL-style mix for SIMD4
@inline(__always)
func mix(_ a: SIMD4<CGFloat>, _ b: SIMD4<CGFloat>, t: CGFloat) -> SIMD4<CGFloat> {
return a + (b - a) * t
}

@inline(__always)
func dot(_ a: SIMD2<CGFloat>, _ b: SIMD2<CGFloat>) -> CGFloat {
return a.x * b.x + a.y * b.y
}
5 changes: 5 additions & 0 deletions Sources/CALayer+SDL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ extension CALayer {
}
}

if needsDisplay() {
display()
_needsDisplay = false
}

if let contents = contents {
do {
try renderer.blit(
Expand Down
1 change: 1 addition & 0 deletions Sources/CALayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ open class CALayer {
if !isPresentationForAnotherLayer && bounds.size != newBounds.size {
// It seems weird to access the superview here but it matches the iOS behaviour
(self.superlayer?.delegate as? UIView)?.setNeedsLayout()
self.setNeedsDisplay()
}

}
Expand Down
32 changes: 27 additions & 5 deletions Sources/CGImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,34 @@ public class CGImage {
var data = sourceData

guard let gpuImagePtr = data.withUnsafeMutableBytes({ buffer -> UnsafeMutablePointer<GPU_Image>? in
guard let ptr = buffer.baseAddress?.assumingMemoryBound(to: Int8.self) else {
return nil
var width: Int32 = 0
var height: Int32 = 0
var channels: Int32 = 4

#if os(Android)
// Android natively supports 2-channel textures. Use them to save 50% (GPU) RAM.
let data = stbi_load_from_memory(buffer.baseAddress, Int32(buffer.count), &width, &height, &channels, 0)

let format: GPU_FormatEnum = switch channels {
case 1: GPU_FORMAT_ALPHA
case 2: GPU_FORMAT_LUMINANCE_ALPHA
case 3: GPU_FORMAT_RGB
case 4: GPU_FORMAT_RGBA
default: fatalError()
}

let rw = SDL_RWFromMem(ptr, Int32(buffer.count))
return GPU_LoadImage_RW(rw, true)
#elseif os(macOS)
// OpenGL on macOS does not natively support 2-channel textures (`unit 0 GLD_TEXTURE_INDEX_2D is unloadable`).
// Instead, force `stb_image` to load all images as if they had 4 channels.
// This is more compatible, but requires more memory.
let data = stbi_load_from_memory(buffer.baseAddress, Int32(buffer.count), &width, &height, nil, channels)
let format = GPU_FORMAT_RGBA
#endif

let img = GPU_CreateImage(UInt16(width), UInt16(height), format)
GPU_UpdateImageBytes(img, nil, data, width * channels)
data?.deallocate()

return img
}) else { return nil }

self.init(gpuImagePtr, sourceData: data)
Expand Down
25 changes: 24 additions & 1 deletion Sources/UIApplication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,29 @@ public func nativeProcessEventsAndRender(env: UnsafeMutablePointer<JNIEnv?>?, vi
let frameTime = Timer()
UIApplication.shared?.handleEventsIfNeeded()
UIScreen.main?.render(window: UIApplication.shared?.keyWindow, atTime: frameTime)
dispatchMainQueueCallback(nil)

// <Service the main queue / thread / actor>
// Note: this would be more efficiently and effectively
// served by the newer Swift executor APIs

// Arbitrary, but designed to get "a lot" of work done if needed
// without boosting baseline CPU usage noticeably above 0%:
let N = 64

for _ in 0 ..< N {
// This loop is designed to ensure the main queue is serviced
// thoroughly, especially on shutdown where the frame callback is no
// longer called regularly but there might still be cleanup work to
// do on the Main Actor e.g. upon deinit.
dispatchMainQueueCallback(nil)
if frameTime.elapsedTimeInMilliseconds > 8 {
// Process the main queue at least once per frame, up to N times
// provided that processing doesn't take us beyond 50% of the
// frame budget @60fps.
break
}
}

// </Service the main queue / thread / actor>
}
#endif
15 changes: 5 additions & 10 deletions Sources/UIApplicationDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,13 @@ public extension UIApplicationDelegate {
func applicationWillResignActive(_ application: UIApplication) {}
func applicationDidEnterBackground(_ application: UIApplication) {}

// Note: this is not used on Android, because there we have a library, so no `main` function will be called.
@MainActor
static func main() async throws {
#if os(macOS)
// On Mac (like on iOS), the main thread blocks here via RunLoop.current.run().
defer { setupRenderAndRunLoop() }
#else
// Android is handled differently: we don't want to block the main thread because the system needs it.
// Instead, we call render periodically from Kotlin via the Android Choreographer API (see UIApplication).
// That said, this function won't even be called on platforms like Android where the app is built as a library, not an executable.
#endif

#if !os(Android) // Unused on Android: we build a library, so no `main` function gets called.
_ = UIApplicationMain(UIApplication.self, Self.self)

// On Mac (like on iOS), the main thread blocks here via RunLoop.current.run().
setupRenderAndRunLoop()
#endif // !os(Android)
}
}
6 changes: 6 additions & 0 deletions Sources/androidNativeInit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ public func nativeDestroyScreen(env: UnsafeMutablePointer<JNIEnv>, view: JavaObj
UIApplication.onWillEnterBackground()
UIApplication.onDidEnterBackground()
}

@MainActor
@_cdecl("Java_org_libsdl_app_SDLActivity_onNativeShouldRelayout")
public func onNativeShouldRelayout(env: UnsafeMutablePointer<JNIEnv>, view: JavaObject) {
UIApplication.shared?.keyWindow?.setNeedsLayout()
}
#endif
Loading