Skip to content

fix: 並列AVIFデコード時のクラッシュを修正#6

Merged
SeiyaTozaki merged 2 commits into
masterfrom
fix/thread-safe-colorspace-cache
Jun 2, 2026
Merged

fix: 並列AVIFデコード時のクラッシュを修正#6
SeiyaTozaki merged 2 commits into
masterfrom
fix/thread-safe-colorspace-cache

Conversation

@SeiyaTozaki

@SeiyaTozaki SeiyaTozaki commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Nuke で AVIF 画像を複数枚同時に読み込むと、まれにクラッシュする問題を修正しました。

アプリ側の設定ミスではなく、ライブラリ内部の ColorSpace キャッシュがスレッドセーフでなかったことが原因です。Glenwood など、漫画ページの prefetch を多用するアプリで報告されていました。


このライブラリは何をしているか

本プラグインは Nuke の画像読み込みパイプラインに AVIF デコーダを追加し、.avif ファイルを UIImage に変換します。

NukeExtensions.loadImage(url)
    ↓ ① urlのデータがキャッシュされていなければfetchする
// ここからプラグイン内で処理
返ってきた.avif ファイル(圧縮データ)
    ↓ ② libavif(Cで書かれたAVIF変換ライブラリ) でデコード
YUV 形式のピクセルデータ(明るさ + 色差)
    ↓ ③ Apple vImage で色変換
RGB / グレースケールのピクセルデータ
    ↓ ④ ColorSpace(sRGB 等)を付与して CGImage 化
UIImage 生成
// ここまで
 ↓ ⑤ 生成されたImageをresponseに入れて返す
avifの中身が反映されたUIImage型の画像などを取得

アプリ側でAvifImageDecoder.enable()をすることで、Nukeのデコーダリストにプラグインのデコーダが追加されるので、受け取った画像ファイルがavifだった場合プラグインのデコーダが使用される

クラッシュはどこで起きていたか

スタックトレース上、③ の最終段階(CGImage 生成時の ColorSpace 取得) で落ちていました。

Nuke(画像ダウンロード・キャッシュ)
  → バックグラウンドスレッドで AvifImageDecoder.decode()
    → YUV → RGB 変換(ここまでは成功)
      → CGImage.create()
        → calcColorSpaceRGB()  ★ ここでクラッシュ

Crashlytics では swift_isUniquelyReferenced 付近、CGImageCreation.swift:15 として記録されていました。

クラッシュ箇所では何をしているのか

このライブラリはavifファイルから得られたピクセル情報などから、CGImageを作っています
この方法でCGImageを作る上で必要な要素の一つとして"CGColorSpace"があります

static func create(from avif: avifImage, characteristics: Characteristics, buffer: vImage_Buffer) throws -> CGImage {
guard let provider = CGDataProvider(dataInfo: nil, data: buffer.data, size: buffer.rowBytes * Int(buffer.height), releaseData: { info, data, size in data.deallocate() }) else { throw CGDataProviderCreationError() }
let colorSpace = characteristics.monochrome ? try calcColorSpaceMonochrome(avif: avif) : try calcColorSpaceRGB(avif: avif)
let imageRef = CGImage(width: avif.iWidth,
height: avif.iHeight,
bitsPerComponent: 8,
bitsPerPixel: characteristics.componentsPerPixel * 8,
bytesPerRow: characteristics.bytesPerRow,
space: colorSpace,
bitmapInfo: .init(rawValue: (characteristics.hasAlpha ? CGImageAlphaInfo.first : CGImageAlphaInfo.none).rawValue),
provider: provider,
decode: nil,
shouldInterpolate: false,
intent: .defaultIntent)
guard let imageRef = imageRef else { throw CGImageCreationError() }
return imageRef
}

ColorSpace(色空間)は、ピクセル値を「どんな色として解釈するか」を定義する情報です。
AVIF ファイルには ICC プロファイルや CICP(色原色・トーンカーブ)としてこの情報が含まれており、デコード時に CGColorSpace として CGImage に付けます。
付けないと OS が別の色空間(例: デバイス RGB)で解釈するため、意図した色と表示がずれることがあります。

avifファイルに保存されていた色情報からColorSpaceを生成する役割を"calcColorSpaceMonochrome" or "calcColorSpaceRGB"が担っています。そこで今回はクラッシュしていました。

なぜクラッシュしたか

ColorSpaceを アプリ全体で共有する Dictionary キャッシュ から取得していました。

private var rgbColorSpaces = [CFString : CGColorSpace]()

func calcColorSpaceRGB(avif: avifImage) throws -> CGColorSpace {
    // 画像固有のICCプロファイルがあれば優先
    if avif.icc.data != nil && avif.icc.size > 0 {
        guard let iccData = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, avif.icc.data, avif.icc.size, kCFAllocatorNull) else { throw ColorSpaceError(message: "cfdata creation failed.") }
        guard let colorSpace = CGColorSpace(iccData: iccData) else { throw ColorSpaceError(message: "colorspace creation failed.") }
        return colorSpace
    }
    
    // キャッシュされていたらその情報を返し、なかったら生成する
    // ここで複数スレッドからの読み書きが起きてクラッシュ
    func cachedColorSpace(identifier: CFString) -> CGColorSpace {
        if let colorSpace = rgbColorSpaces[identifier] {
            return colorSpace
        } else {
            let colorSpace = CGColorSpace(name: identifier)!
            rgbColorSpaces[identifier] = colorSpace
            return colorSpace
        }
    }
    
    // avifの中の色情報から対応するカラースペースを作成
    switch (avif.colorPrimaries, avif.transferCharacteristics) {
    case (AVIF_COLOR_PRIMARIES_BT709, AVIF_TRANSFER_CHARACTERISTICS_BT709):
        return cachedColorSpace(identifier: CGColorSpace.itur_709)
        
    case (AVIF_COLOR_PRIMARIES_BT709, AVIF_TRANSFER_CHARACTERISTICS_SRGB):
        return cachedColorSpace(identifier: CGColorSpace.sRGB)

    // 後略
}

Nuke は画像 decode を 複数スレッドで並列実行 します。
漫画アプリのように 複数ページを prefetch する と、初回 decode 時に複数スレッドが同時にキャッシュへ書き込み、Dictionary の内部状態が壊れてクラッシュしていました。


今回の修正内容

1. ColorSpace キャッシュのスレッドセーフ化(本修正)

NSLock でキャッシュへのアクセスを直列化し、並列 decode しても安全にしました。
デコード確認結果↓
https://www.notion.so/link-u/Nuke-avif-plugin-36e5e0d3605d8098bb2df1c3802bb586?source=copy_link#36e5e0d3605d809aae55c040a243941e

2. その他のバグ修正(副次)

ファイル 内容
BufferConversion.swift YUV422 変換で Cr バッファの参照先が誤っていた typo を修正
BufferExtraction.swift ダミー Cr プレーンの確保サイズを Cb 側と統一
ColorSpace.swift 重複していた switch case を整理

SeiyaTozaki and others added 2 commits May 19, 2026 18:14
Nukeが複数スレッドでAVIF画像をデコードする際、ColorSpaceキャッシュの
Dictionary CoW競合でクラッシュしていた問題をNSLockで保護して修正。

あわせてYUV422のypCrDiffBufferのバッファ参照ミスと、
ダミーCrプレーンの確保サイズ不整合も修正。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes a crash that occurred when Nuke decoded multiple AVIF images concurrently. The root cause was non-thread-safe Dictionary caches for CGColorSpace shared across decode threads; this PR serializes access via NSLock. Two unrelated bug fixes in the YUV buffer code are also included.

Changes:

  • Introduce a private ColorSpaceCache enum guarding RGB/monochrome CGColorSpace dictionaries with NSLock, and remove the file-scope mutable dictionaries.
  • Collapse the duplicated (UNKNOWN, UNKNOWN) switch cases in both calcColorSpaceRGB and calcColorSpaceMonochrome.
  • Fix two typos in the buffer pipeline: dummy Cr allocation size now uses MemoryLayout<UInt8> (matching Cb), and YUV422 ypCrDiffBuffer now points at ypCrDiffBufferData instead of ypCbDiffBufferData.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
Nuke-Avif-Plugin/ColorSpace.swift Adds NSLock-guarded ColorSpaceCache, routes both cache helpers through it, deduplicates UNKNOWN switch arms.
Nuke-Avif-Plugin/Buffer/BufferExtraction.swift Fixes dummy Cr buffer allocation size to UInt8 (was Int).
Nuke-Avif-Plugin/Buffer/BufferConversion.swift Fixes YUV422 Cr diff buffer to reference ypCrDiffBufferData instead of ypCbDiffBufferData.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@SeiyaTozaki SeiyaTozaki merged commit 1c823a0 into master Jun 2, 2026
1 check passed
@SeiyaTozaki SeiyaTozaki deleted the fix/thread-safe-colorspace-cache branch June 2, 2026 00:51
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.

3 participants