Fix: Swift 6 concurrency - async render + Sendable wrapper
Some checks are pending
Auto Tag on Push / auto-tag (push) Waiting to run
Some checks are pending
Auto Tag on Push / auto-tag (push) Waiting to run
This commit is contained in:
parent
586f87e222
commit
38868a2aba
|
|
@ -1,4 +1,4 @@
|
||||||
import Metal
|
@preconcurrency import Metal
|
||||||
import MetalKit
|
import MetalKit
|
||||||
import CoreGraphics
|
import CoreGraphics
|
||||||
|
|
||||||
|
|
@ -30,7 +30,8 @@ struct RenderParameters {
|
||||||
var randomSeed: UInt32
|
var randomSeed: UInt32
|
||||||
}
|
}
|
||||||
|
|
||||||
final class MetalImageRenderer: Sendable {
|
@MainActor
|
||||||
|
final class MetalImageRenderer {
|
||||||
private let device: MTLDevice
|
private let device: MTLDevice
|
||||||
private let commandQueue: MTLCommandQueue
|
private let commandQueue: MTLCommandQueue
|
||||||
private let pipelineState: MTLComputePipelineState
|
private let pipelineState: MTLComputePipelineState
|
||||||
|
|
@ -65,8 +66,9 @@ final class MetalImageRenderer: Sendable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(input: CGImage, params: RenderParameters) -> CGImage? {
|
func render(input: CGImage, params: RenderParameters) async -> CGImage? {
|
||||||
return autoreleasepool {
|
return await withCheckedContinuation { continuation in
|
||||||
|
autoreleasepool {
|
||||||
print("🎨 Metal render started - Image: \(input.width)x\(input.height), Algo: \(params.algorithm)")
|
print("🎨 Metal render started - Image: \(input.width)x\(input.height), Algo: \(params.algorithm)")
|
||||||
|
|
||||||
let textureLoader = MTKTextureLoader(device: device)
|
let textureLoader = MTKTextureLoader(device: device)
|
||||||
|
|
@ -74,7 +76,8 @@ final class MetalImageRenderer: Sendable {
|
||||||
// Load input texture
|
// Load input texture
|
||||||
guard let inputTexture = try? textureLoader.newTexture(cgImage: input, options: [.origin: MTKTextureLoader.Origin.topLeft]) else {
|
guard let inputTexture = try? textureLoader.newTexture(cgImage: input, options: [.origin: MTKTextureLoader.Origin.topLeft]) else {
|
||||||
print("❌ Failed to create input texture")
|
print("❌ Failed to create input texture")
|
||||||
return nil
|
continuation.resume(returning: nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("✅ Input texture created: \(inputTexture.width)x\(inputTexture.height)")
|
print("✅ Input texture created: \(inputTexture.width)x\(inputTexture.height)")
|
||||||
|
|
@ -88,14 +91,16 @@ final class MetalImageRenderer: Sendable {
|
||||||
|
|
||||||
guard let outputTexture = device.makeTexture(descriptor: descriptor) else {
|
guard let outputTexture = device.makeTexture(descriptor: descriptor) else {
|
||||||
print("❌ Failed to create output texture")
|
print("❌ Failed to create output texture")
|
||||||
return nil
|
continuation.resume(returning: nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode command
|
// Encode command
|
||||||
guard let commandBuffer = commandQueue.makeCommandBuffer(),
|
guard let commandBuffer = commandQueue.makeCommandBuffer(),
|
||||||
let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
|
let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
|
||||||
print("❌ Failed to create command buffer or encoder")
|
print("❌ Failed to create command buffer or encoder")
|
||||||
return nil
|
continuation.resume(returning: nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var params = params
|
var params = params
|
||||||
|
|
@ -115,7 +120,8 @@ final class MetalImageRenderer: Sendable {
|
||||||
// CRITICAL: Use autoreleasepool check for error texture too
|
// CRITICAL: Use autoreleasepool check for error texture too
|
||||||
guard let errorTexture = device.makeTexture(descriptor: errorDesc) else {
|
guard let errorTexture = device.makeTexture(descriptor: errorDesc) else {
|
||||||
computeEncoder.endEncoding()
|
computeEncoder.endEncoding()
|
||||||
return nil
|
continuation.resume(returning: nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// PASS 1: Even Rows
|
// PASS 1: Even Rows
|
||||||
|
|
@ -163,22 +169,44 @@ final class MetalImageRenderer: Sendable {
|
||||||
|
|
||||||
computeEncoder.endEncoding()
|
computeEncoder.endEncoding()
|
||||||
|
|
||||||
commandBuffer.commit()
|
// Add completion handler properly inside the closure
|
||||||
commandBuffer.waitUntilCompleted()
|
commandBuffer.addCompletedHandler { buffer in
|
||||||
|
// We must jump back to MainActor if we want to do UI stuff, but here we just process data.
|
||||||
|
// However, continuation must be resumed.
|
||||||
|
// Since the whole function is @MainActor, we should likely resume on main actor?
|
||||||
|
// Actually, withCheckedContinuation handles the resume context automatically or acts as a bridge.
|
||||||
|
// But to be safe and strict, let's keep it simple.
|
||||||
|
|
||||||
if let error = commandBuffer.error {
|
if let error = buffer.error {
|
||||||
print("❌ Metal command buffer error: \(error)")
|
print("❌ Metal command buffer error: \(error)")
|
||||||
return nil
|
continuation.resume(returning: nil)
|
||||||
}
|
} else {
|
||||||
|
|
||||||
print("✅ Metal render completed successfully")
|
print("✅ Metal render completed successfully")
|
||||||
|
// Texture -> CGImage conversion is fast enough to do here or dispatch to main
|
||||||
|
// But since createCGImage creates data copies, it is safe.
|
||||||
|
// We need the result.
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// We are back on main thread (required for MetalImageRenderer methods if isolated)
|
||||||
|
// But wait, makeCGImage is private and inside this class.
|
||||||
|
// If we call self.createCGImage here, we are inside a closure which is NOT isolated to MainActor by default unless specified.
|
||||||
|
// Let's call a helper or do it carefully.
|
||||||
|
|
||||||
let result = createCGImage(from: outputTexture)
|
// BETTER APPROACH:
|
||||||
|
// Just resume with the texture or nil, and do conversion after await?
|
||||||
|
// OR: perform conversion here.
|
||||||
|
|
||||||
|
// Since `createCGImage` is private and self is MainActor, we must be on MainActor to call it.
|
||||||
|
let result = self.createCGImage(from: outputTexture)
|
||||||
if result == nil {
|
if result == nil {
|
||||||
print("❌ Failed to create CGImage from output texture")
|
print("❌ Failed to create CGImage from output texture")
|
||||||
}
|
}
|
||||||
|
continuation.resume(returning: result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
commandBuffer.commit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ import ImageIO
|
||||||
import AppKit
|
import AppKit
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
// Helper for Swift 6 Concurrency
|
||||||
|
struct SendableCGImage: @unchecked Sendable {
|
||||||
|
let image: CGImage
|
||||||
|
}
|
||||||
|
|
||||||
enum DitherAlgorithm: Int, CaseIterable, Identifiable {
|
enum DitherAlgorithm: Int, CaseIterable, Identifiable {
|
||||||
case noDither = 0
|
case noDither = 0
|
||||||
case bayer2x2 = 1
|
case bayer2x2 = 1
|
||||||
|
|
@ -175,27 +180,29 @@ class DitherViewModel {
|
||||||
|
|
||||||
print("🔄 Processing image with algorithm: \(self.selectedAlgorithm.name)")
|
print("🔄 Processing image with algorithm: \(self.selectedAlgorithm.name)")
|
||||||
|
|
||||||
self.renderTask = Task.detached(priority: .userInitiated) { [input, renderer, params] in
|
// Wrap CGImage in a Sendable wrapper to satisfy strict concurrency
|
||||||
|
let inputWrapper = SendableCGImage(image: input)
|
||||||
|
|
||||||
|
self.renderTask = Task { @MainActor [renderer, params, inputWrapper] in
|
||||||
if Task.isCancelled {
|
if Task.isCancelled {
|
||||||
print("⚠️ Render task cancelled before starting")
|
print("⚠️ Render task cancelled before starting")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = renderer.render(input: input, params: params)
|
// Call async render method
|
||||||
|
let result = await renderer.render(input: inputWrapper.image, params: params)
|
||||||
|
|
||||||
if Task.isCancelled {
|
if Task.isCancelled {
|
||||||
print("⚠️ Render task cancelled after render")
|
print("⚠️ Render task cancelled after render")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
if Task.isCancelled { return }
|
if Task.isCancelled { return }
|
||||||
print("✅ Render complete, updating UI")
|
print("✅ Render complete, updating UI")
|
||||||
self.processedImage = result
|
self.processedImage = result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func exportResult(to url: URL) {
|
func exportResult(to url: URL) {
|
||||||
// Legacy export, keeping for compatibility but forwarding to new system with defaults
|
// Legacy export, keeping for compatibility but forwarding to new system with defaults
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue