V0.1.4.2 FIxed memory leak issue AND an import issue, causing re-importation not working properly
This commit is contained in:
parent
a470a232e8
commit
2d281fcf39
|
|
@ -343,7 +343,7 @@ struct SidebarView: View {
|
|||
.foregroundStyle(.secondary.opacity(0.8))
|
||||
.padding(.top, 12)
|
||||
|
||||
SliderControl(label: "Pixel Displace", value: $viewModel.pixelDisplace, range: 0...50, format: .pixels)
|
||||
SliderControl(label: "Pixel Displace", value: $viewModel.pixelDisplace, range: 0...100, format: .pixels)
|
||||
SliderControl(label: "Turbulence", value: $viewModel.turbulence, range: 0...1, format: .percent)
|
||||
SliderControl(label: "Chroma Aberration", value: $viewModel.chromaAberration, range: 0...20, format: .pixels)
|
||||
|
||||
|
|
@ -379,6 +379,16 @@ struct SidebarView: View {
|
|||
.cornerRadius(8)
|
||||
.shadow(color: .black.opacity(0.05), radius: 1, x: 0, y: 1)
|
||||
|
||||
#if DEBUG
|
||||
Button("Force Refresh (Debug)") {
|
||||
viewModel.forceRefresh()
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
.buttonStyle(.plain)
|
||||
.padding(.leading, 4)
|
||||
#endif
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(20)
|
||||
|
|
|
|||
|
|
@ -66,13 +66,19 @@ final class MetalImageRenderer: Sendable {
|
|||
}
|
||||
|
||||
func render(input: CGImage, params: RenderParameters) -> CGImage? {
|
||||
return autoreleasepool {
|
||||
print("🎨 Metal render started - Image: \(input.width)x\(input.height), Algo: \(params.algorithm)")
|
||||
|
||||
let textureLoader = MTKTextureLoader(device: device)
|
||||
|
||||
// Load input texture
|
||||
guard let inputTexture = try? textureLoader.newTexture(cgImage: input, options: [.origin: MTKTextureLoader.Origin.topLeft]) else {
|
||||
print("❌ Failed to create input texture")
|
||||
return nil
|
||||
}
|
||||
|
||||
print("✅ Input texture created: \(inputTexture.width)x\(inputTexture.height)")
|
||||
|
||||
// Create output texture
|
||||
let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm,
|
||||
width: inputTexture.width,
|
||||
|
|
@ -81,18 +87,22 @@ final class MetalImageRenderer: Sendable {
|
|||
descriptor.usage = [.shaderWrite, .shaderRead]
|
||||
|
||||
guard let outputTexture = device.makeTexture(descriptor: descriptor) else {
|
||||
print("❌ Failed to create output texture")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode command
|
||||
guard let commandBuffer = commandQueue.makeCommandBuffer(),
|
||||
let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
|
||||
print("❌ Failed to create command buffer or encoder")
|
||||
return nil
|
||||
}
|
||||
|
||||
var params = params
|
||||
|
||||
if params.algorithm == 7, let pipe1 = pipelineStateFS_Pass1, let pipe2 = pipelineStateFS_Pass2 {
|
||||
print("🔄 Using Floyd-Steinberg two-pass rendering")
|
||||
|
||||
// FLOYD-STEINBERG MULTI-PASS
|
||||
|
||||
// Create Error Texture (Float16 or Float32 for precision)
|
||||
|
|
@ -101,6 +111,8 @@ final class MetalImageRenderer: Sendable {
|
|||
height: inputTexture.height,
|
||||
mipmapped: false)
|
||||
errorDesc.usage = [.shaderWrite, .shaderRead]
|
||||
|
||||
// CRITICAL: Use autoreleasepool check for error texture too
|
||||
guard let errorTexture = device.makeTexture(descriptor: errorDesc) else {
|
||||
computeEncoder.endEncoding()
|
||||
return nil
|
||||
|
|
@ -133,6 +145,8 @@ final class MetalImageRenderer: Sendable {
|
|||
computeEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup)
|
||||
|
||||
} else {
|
||||
print("🔄 Using standard dithering algorithm")
|
||||
|
||||
// STANDARD ALGORITHMS
|
||||
computeEncoder.setComputePipelineState(pipelineState)
|
||||
computeEncoder.setTexture(inputTexture, index: 0)
|
||||
|
|
@ -152,7 +166,20 @@ final class MetalImageRenderer: Sendable {
|
|||
commandBuffer.commit()
|
||||
commandBuffer.waitUntilCompleted()
|
||||
|
||||
return createCGImage(from: outputTexture)
|
||||
if let error = commandBuffer.error {
|
||||
print("❌ Metal command buffer error: \(error)")
|
||||
return nil
|
||||
}
|
||||
|
||||
print("✅ Metal render completed successfully")
|
||||
|
||||
let result = createCGImage(from: outputTexture)
|
||||
if result == nil {
|
||||
print("❌ Failed to create CGImage from output texture")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private func createCGImage(from texture: MTLTexture) -> CGImage? {
|
||||
|
|
@ -161,6 +188,7 @@ final class MetalImageRenderer: Sendable {
|
|||
let rowBytes = width * 4
|
||||
let length = rowBytes * height
|
||||
|
||||
// CRITICAL: Create data buffer that will be copied, not retained
|
||||
var bytes = [UInt8](repeating: 0, count: length)
|
||||
let region = MTLRegionMake2D(0, 0, width, height)
|
||||
|
||||
|
|
@ -169,7 +197,9 @@ final class MetalImageRenderer: Sendable {
|
|||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
|
||||
|
||||
guard let provider = CGDataProvider(data: Data(bytes: bytes, count: length) as CFData) else { return nil }
|
||||
// Create data with .copy behavior to avoid retaining original buffer
|
||||
guard let data = CFDataCreate(nil, bytes, length) else { return nil }
|
||||
guard let provider = CGDataProvider(data: data) else { return nil }
|
||||
|
||||
return CGImage(width: width,
|
||||
height: height,
|
||||
|
|
|
|||
|
|
@ -65,14 +65,14 @@ float2 applySpatialChaos(float2 coord, constant RenderParameters ¶ms, uint2
|
|||
|
||||
if (params.pixelDisplace > 0.0) {
|
||||
float2 offset = random2(coord * 0.01, params.randomSeed) - 0.5;
|
||||
chaosCoord += offset * params.pixelDisplace;
|
||||
chaosCoord += offset * params.pixelDisplace * 2.0;
|
||||
}
|
||||
|
||||
if (params.turbulence > 0.0) {
|
||||
float scale = 0.05;
|
||||
float scale = 0.02;
|
||||
float offsetX = noise(coord * scale, params.randomSeed) * 2.0 - 1.0;
|
||||
float offsetY = noise(coord * scale + float2(100.0), params.randomSeed) * 2.0 - 1.0;
|
||||
chaosCoord += float2(offsetX, offsetY) * params.turbulence * 20.0;
|
||||
chaosCoord += float2(offsetX, offsetY) * params.turbulence * 50.0;
|
||||
}
|
||||
|
||||
return chaosCoord;
|
||||
|
|
@ -123,8 +123,8 @@ float3 applyChromaAberration(texture2d<float, access::read> inputTexture,
|
|||
return inputTexture.read(pixelCoord).rgb;
|
||||
}
|
||||
|
||||
float2 redOffset = coord + float2(amount, 0);
|
||||
float2 blueOffset = coord - float2(amount, 0);
|
||||
float2 redOffset = coord + float2(amount, amount * 0.5);
|
||||
float2 blueOffset = coord - float2(amount, amount * 0.5);
|
||||
|
||||
uint2 redCoord = uint2(clamp(redOffset, float2(0), float2(texSize) - 1.0));
|
||||
uint2 greenCoord = uint2(clamp(coord, float2(0), float2(texSize) - 1.0));
|
||||
|
|
@ -141,15 +141,15 @@ float applyQuantizationChaos(float value, float2 coord, constant RenderParameter
|
|||
float chaosValue = value;
|
||||
|
||||
if (params.bitDepthChaos > 0.0) {
|
||||
float randVal = random(coord * 0.1, params.randomSeed);
|
||||
float randVal = random(coord * 0.05, params.randomSeed);
|
||||
if (randVal < params.bitDepthChaos) {
|
||||
float reducedDepth = floor(randVal * 3.0) + 2.0;
|
||||
float reducedDepth = floor(randVal * 1.5) + 2.0;
|
||||
chaosValue = floor(value * reducedDepth) / reducedDepth;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.paletteRandomize > 0.0) {
|
||||
float randShift = (random(coord, params.randomSeed) - 0.5) * params.paletteRandomize;
|
||||
float randShift = (random(coord, params.randomSeed) - 0.5) * params.paletteRandomize * 0.5;
|
||||
chaosValue = clamp(value + randShift, 0.0, 1.0);
|
||||
}
|
||||
|
||||
|
|
@ -409,7 +409,13 @@ kernel void ditherShaderFS_Pass1(texture2d<float, access::read> inputTexture [[t
|
|||
float weight = 7.0 / 16.0;
|
||||
if (params.errorRandomness > 0.0) {
|
||||
float r = random(float2(coords), params.randomSeed);
|
||||
weight = mix(weight, r * 0.8, params.errorRandomness);
|
||||
if (r < params.errorRandomness * 1.5) {
|
||||
float r1 = random(float2(coords) + float2(1.0), params.randomSeed);
|
||||
float r2 = random(float2(coords) + float2(2.0), params.randomSeed);
|
||||
r1 = pow(r1, 0.5);
|
||||
r2 = pow(r2, 0.5);
|
||||
weight = mix(weight, r1, params.errorRandomness);
|
||||
}
|
||||
}
|
||||
|
||||
currentError = diff * weight;
|
||||
|
|
@ -447,11 +453,18 @@ kernel void ditherShaderFS_Pass2(texture2d<float, access::read> inputTexture [[t
|
|||
// Chaos: Error Randomness
|
||||
if (params.errorRandomness > 0.0) {
|
||||
float r = random(float2(coords) + float2(10.0), params.randomSeed);
|
||||
if (r < params.errorRandomness) {
|
||||
if (r < params.errorRandomness * 1.5) {
|
||||
float r1 = random(float2(coords) + float2(1.0), params.randomSeed);
|
||||
float r2 = random(float2(coords) + float2(2.0), params.randomSeed);
|
||||
float r3 = random(float2(coords) + float2(3.0), params.randomSeed);
|
||||
float sum = r1 + r2 + r3 + 0.1;
|
||||
float r4 = random(float2(coords) + float2(4.0), params.randomSeed);
|
||||
|
||||
r1 = pow(r1, 0.5);
|
||||
r2 = pow(r2, 0.5);
|
||||
r3 = pow(r3, 0.5);
|
||||
r4 = pow(r4, 0.5);
|
||||
|
||||
float sum = r1 + r2 + r3 + r4 + 0.001;
|
||||
w_tr = r1 / sum;
|
||||
w_t = r2 / sum;
|
||||
w_tl = r3 / sum;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class DitherViewModel {
|
|||
|
||||
private let renderer = MetalImageRenderer()
|
||||
private var renderTask: Task<Void, Never>?
|
||||
private var renderDebounceTask: Task<Void, Never>?
|
||||
|
||||
init() {}
|
||||
|
||||
|
|
@ -78,7 +79,40 @@ class DitherViewModel {
|
|||
processImage()
|
||||
}
|
||||
|
||||
func forceRefresh() {
|
||||
print("🔄 Force refresh triggered")
|
||||
guard let _ = inputImage else {
|
||||
print("⚠️ No input image to refresh")
|
||||
return
|
||||
}
|
||||
|
||||
// Clear everything
|
||||
renderDebounceTask?.cancel()
|
||||
renderDebounceTask = nil
|
||||
renderTask?.cancel()
|
||||
renderTask = nil
|
||||
processedImage = nil
|
||||
|
||||
// Wait a frame
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(for: .milliseconds(100))
|
||||
self.processImage()
|
||||
}
|
||||
}
|
||||
|
||||
func load(url: URL) {
|
||||
// Cancel all tasks
|
||||
renderTask?.cancel()
|
||||
renderTask = nil
|
||||
renderDebounceTask?.cancel()
|
||||
renderDebounceTask = nil
|
||||
|
||||
// CRITICAL: Clear old images to release memory
|
||||
processedImage = nil
|
||||
inputImage = nil
|
||||
|
||||
// Force memory cleanup and load new image
|
||||
autoreleasepool {
|
||||
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
|
||||
let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
|
||||
print("Failed to load image from \(url)")
|
||||
|
|
@ -86,54 +120,82 @@ class DitherViewModel {
|
|||
}
|
||||
|
||||
self.inputImage = cgImage
|
||||
}
|
||||
|
||||
self.inputImageId = UUID() // Signal that a new image has been loaded
|
||||
|
||||
// Small delay to ensure UI updates
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(for: .milliseconds(100))
|
||||
self.processImage()
|
||||
}
|
||||
}
|
||||
|
||||
func processImage() {
|
||||
guard let input = inputImage, let renderer = renderer else { return }
|
||||
|
||||
// Cancel previous task to prevent UI freezing and Metal overload
|
||||
renderTask?.cancel()
|
||||
// Cancel previous debounce
|
||||
renderDebounceTask?.cancel()
|
||||
|
||||
// Debounce rapid parameter changes
|
||||
renderDebounceTask = Task { @MainActor in
|
||||
try? await Task.sleep(for: .milliseconds(50)) // 50ms debounce
|
||||
|
||||
if Task.isCancelled { return }
|
||||
|
||||
// Cancel previous render task
|
||||
self.renderTask?.cancel()
|
||||
self.renderTask = nil
|
||||
|
||||
// Generate a random seed for consistent chaos per frame/update
|
||||
let seed = UInt32.random(in: 0...UInt32.max)
|
||||
|
||||
let params = RenderParameters(
|
||||
brightness: Float(brightness),
|
||||
contrast: Float(contrast),
|
||||
pixelScale: Float(pixelScale),
|
||||
colorDepth: Float(colorDepth),
|
||||
algorithm: Int32(selectedAlgorithm.rawValue),
|
||||
isGrayscale: isGrayscale ? 1 : 0,
|
||||
brightness: Float(self.brightness),
|
||||
contrast: Float(self.contrast),
|
||||
pixelScale: Float(self.pixelScale),
|
||||
colorDepth: Float(self.colorDepth),
|
||||
algorithm: Int32(self.selectedAlgorithm.rawValue),
|
||||
isGrayscale: self.isGrayscale ? 1 : 0,
|
||||
|
||||
// Chaos Params
|
||||
offsetJitter: Float(offsetJitter),
|
||||
patternRotation: Float(patternRotation),
|
||||
errorAmplify: Float(errorAmplify),
|
||||
errorRandomness: Float(errorRandomness),
|
||||
thresholdNoise: Float(thresholdNoise),
|
||||
waveDistortion: Float(waveDistortion),
|
||||
pixelDisplace: Float(pixelDisplace),
|
||||
turbulence: Float(turbulence),
|
||||
chromaAberration: Float(chromaAberration),
|
||||
bitDepthChaos: Float(bitDepthChaos),
|
||||
paletteRandomize: Float(paletteRandomize),
|
||||
offsetJitter: Float(self.offsetJitter),
|
||||
patternRotation: Float(self.patternRotation),
|
||||
errorAmplify: Float(self.errorAmplify),
|
||||
errorRandomness: Float(self.errorRandomness),
|
||||
thresholdNoise: Float(self.thresholdNoise),
|
||||
waveDistortion: Float(self.waveDistortion),
|
||||
pixelDisplace: Float(self.pixelDisplace),
|
||||
turbulence: Float(self.turbulence),
|
||||
chromaAberration: Float(self.chromaAberration),
|
||||
bitDepthChaos: Float(self.bitDepthChaos),
|
||||
paletteRandomize: Float(self.paletteRandomize),
|
||||
randomSeed: seed
|
||||
)
|
||||
|
||||
renderTask = Task.detached(priority: .userInitiated) { [input, renderer, params] in
|
||||
if Task.isCancelled { return }
|
||||
print("🔄 Processing image with algorithm: \(self.selectedAlgorithm.name)")
|
||||
|
||||
self.renderTask = Task.detached(priority: .userInitiated) { [input, renderer, params] in
|
||||
if Task.isCancelled {
|
||||
print("⚠️ Render task cancelled before starting")
|
||||
return
|
||||
}
|
||||
|
||||
let result = renderer.render(input: input, params: params)
|
||||
|
||||
if Task.isCancelled { return }
|
||||
if Task.isCancelled {
|
||||
print("⚠️ Render task cancelled after render")
|
||||
return
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
if Task.isCancelled { return }
|
||||
print("✅ Render complete, updating UI")
|
||||
self.processedImage = result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exportResult(to url: URL) {
|
||||
// Legacy export, keeping for compatibility but forwarding to new system with defaults
|
||||
|
|
|
|||
Loading…
Reference in a new issue