diff --git a/.github/workflows/ci-native.yml b/.github/workflows/ci-native.yml index 73d0388..5481f6e 100644 --- a/.github/workflows/ci-native.yml +++ b/.github/workflows/ci-native.yml @@ -412,12 +412,17 @@ jobs: run: ./tools/dev.ps1 build -Platform ${{ matrix.platform }} # **M7b Task 1: この job には tools/verify-exported-symbols.ps1 を - # 配線しない。** Android の .so は host の nm で読めるが、iOS と Web は - # 静的ライブラリで「エクスポート」という概念自体が違う(ar archive の - # 中の object は動的リンカのエクスポート表を持たない。実際に検査したいのは - # 静的リンク後 —— アプリの実行ファイルに組み込まれた時点 —— の解決なので、 - # このレーンの `.a` に対しては意味を成さない)。desktop 3 job(Windows / - # macOS / Linux)だけが対象である。 + # 配線しない。** iOS と Web は静的ライブラリで「エクスポート」という + # 概念自体が違う(ar archive の中の object は動的リンカのエクスポート表を + # 持たない。実際に検査したいのは静的リンク後 —— アプリの実行ファイルに + # 組み込まれた時点 —— の解決なので、このレーンの `.a` に対しては意味を + # 成さない)。Android は共有ライブラリで概念上は当てはまるが、 + # **host(この job のランナー)の `nm` が arm64 の ELF を読めるかは + # ここでは確認していない**——確認するまでは skip する。desktop 3 job + # (Windows / macOS / Linux)だけが対象である(M7c のレビューで + # release.yml 側の同種のコメントと表現が食い違っていたのを揃えた —— + # この branch で Android の strip 対応を足したことで、この問いに + # 意味のある答えが要る場面が増えている)。 # **配る形になっていること。** Android は 16 KB page size、iOS は # 静的ライブラリであること。どちらも「ビルドできた ≠ 動く」の距離を diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a07d8fd..f108b0c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -326,7 +326,14 @@ jobs: ./tools/verify-exported-symbols.ps1 -LibraryPath $bins[0].FullName if ($LASTEXITCODE -ne 0) { exit 1 } } elseif ($platform -in @('android-arm64', 'ios-arm64', 'web-wasm')) { - Write-Host "==> skipped on ${platform} (iOS / Web は静的ライブラリ、Android は host の nm で読めるかを確かめていない)" + # iOS / Web は静的ライブラリで「エクスポート」の概念自体が + # 違う。Android は共有ライブラリで概念上は当てはまるが、 + # host(この job のランナー)の `nm` が arm64 の ELF を + # 読めるかはここでは確認していない——確認するまでは skip + # する(ci-native.yml の同種のコメントと表現を揃えた。 + # M7c で Android を strip するようになったので、この問いは + # 以前より意味を持つ)。 + Write-Host "==> skipped on ${platform} (iOS / Web は静的ライブラリ。Android は host の nm が arm64 ELF を読めるか未確認のため skip)" } else { Write-Error "unknown platform '${platform}': 公開面を検査するかどうかが決まっていない。tools/verify-exported-symbols.ps1 を掛けるか、ここに明示的な除外を足すこと。" exit 1 @@ -431,10 +438,20 @@ jobs: # CI ランナーの負荷に左右されない。時間と違ってここは assert してよい。 # # 上限 100 MB は、実測(v0.3.0、全部入り 69,565,901 バイト = 66 MB)の - # 約 1.5 倍。倍増を捕まえる —— デバッグシンボルの同梱や platform の - # binary の重複といった「何かが大きく壊れた」形は倍以上になる。 - # 関数を足したときの通常の増分は数百 KB〜数 MB なので、34 MB の余裕は - # 何回分ものリリースに相当する。 + # 約 1.5 倍として決めた —— 「34 MB の余裕は何回分ものリリースに相当する」 + # という見積もりだった。**その見積もりは M7c の dnn 追加で外れた** + # (PR #72、run 34319553823): 124,101,923 バイトで実際に落ちた + # (`package が上限を超えた: 124101923 > 104857600`)。原因はデバッグ + # シンボルの同梱でも platform binary の重複でもなく、dnn とは無関係に + # 前から Android の `.so` へ積もっていた DWARF デバッグ情報(86%)だった + # —— dnn は閾値を越えさせただけである。詳細と実測は + # `docs/roadmap.md` の「dnn の配布の形」。 + # + # **上限は上げていない。** `native/CMakeLists.txt` に Android 限定の + # `llvm-strip --strip-unneeded` を足して原因を直し、全部入りは + # 77,528,652 バイトまで戻った(上限に対して約 27 MB の余裕。ローカルで + # 実際にこの run の 6 platform artifact を材料に再現した実測)。 + # 「倍増を捕まえる」という上限の設計意図そのものは変えていない。 # # `pack-upm-tarball.ps1` の既定 512 MB は 1 桁以上緩く、CLAUDE.md 自身が # 「誰も確かめられない」と書いている。同じ誤りを繰り返さないため、 diff --git a/CLAUDE.md b/CLAUDE.md index c90698e..94dda0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **出していないものを明記しておく。** `calib` には `stereoCalibrate`(ステレオ校正)・`calibrateHandEye`・魚眼系がある。**出したのは単眼カメラの校正 1 本である。**(**ここに挙げていた `geometry` の `solvePnP` は、2026-09 の API 拡張で出した。**) -公開 ABI の内訳は次のとおり(**本数を数えるのは `docs/api-map.md` の冒頭だけ**。ここは何が在るかを説明する)。M0/M1 由来の 8 本(`ocvu_get_abi_version`、last-error の取得、status 表の照会、`ocvu_get_opencv_version` / `ocvu_get_build_information`)に、M2 で `Mat` のライフサイクルと buffer 転送の 6 本(`ocvu_mat_create` / `_release` / `_clone` / `_get_info` / `_copy_from_buffer` / `_copy_to_buffer`。`native/src/ocvu_mat_table.cpp`、`native/src/ocvu_mat.cpp`、`native/src/ocvu_mat_buffer.cpp`)、`imgproc` の 3 本(`ocvu_cvt_color` / `ocvu_resize` / `ocvu_gaussian_blur`。`native/src/ocvu_imgproc.cpp`)、L3 のクラッシュ・ハング耐性を実証する `ocvu_debug_crash`(`native/src/ocvu_debug.cpp`)が加わり、M3.5 で `imgcodecs` の 2 本(`ocvu_imencode` / `ocvu_imdecode`。`native/src/ocvu_imgcodecs.cpp`)、続く M5 の module 拡張で `objdetect` の 2 本(`ocvu_qr_encode` / `ocvu_qr_decode`。`native/src/ocvu_objdetect.cpp`)と `features` の 1 本(`ocvu_orb_detect`。`native/src/ocvu_features.cpp`)、`geometry` の 1 本(`ocvu_find_homography`。`native/src/ocvu_geometry.cpp`)、続けてカメラの歪み補正の 2 本(`ocvu_undistort` / `ocvu_find_chessboard_corners`。**`imgproc` と `objdetect` にまたがるが、実装は `native/src/ocvu_calibration.cpp` の 1 ファイルにまとめてある** —— `calib` module は使っていない)が加わった。**2026-09 の API 拡張で 26 本足した** —— `geometry` の姿勢 4 本と `ocvu_get_perspective_transform`(`native/src/ocvu_pose.cpp` と `ocvu_geometry.cpp`)、`objdetect` の ArUco 2 本(`native/src/ocvu_aruco.cpp`)、`imgproc` の 8 本(`native/src/ocvu_imgproc_ops.cpp` と `ocvu_imgproc_shape.cpp`)、`core` の基本演算 8 本(`native/src/ocvu_core_ops.cpp`)、`features` の記述子 2 本(`native/src/ocvu_matching.cpp`)、**そして新設した `stereo` module の 1 本**(`native/src/ocvu_stereo.cpp`)である。**`stereo` は 8 つ目のリンク済み module だが、`tools/opencv-config.psd1` の `Modules` は触っていない** —— `calib` が推移的に引くので OpenCV 側は既にビルドしており、`COMPONENTS` に 1 語足すだけで済んだ(構成ハッシュは変わらず、6 platform 分の再ビルドは起きていない)。所有権・versioning・API allowlist の正本は `docs/abi-ownership-and-versioning.md`。**M5 で宣言の在り処が変わった** —— `extern "C"` の関数宣言は `native/include/opencv_unity_native.h` から module ごとの `native/include/ocvu/{infra,core,imgproc,imgcodecs,objdetect,features,geometry,calib,stereo}.h` へ移り、**いずれも生成物になった**。もとのヘッダが今も持つのは `OCVU_STATUS_LIST`・handle と struct の型・`OCVU_*` 定数だけで、**関数宣言はもう 1 本も無い**(9 つを include するだけである。**その include の行だけは生成物ではない** —— `tools/tests/BindingGenerator.Tests.ps1` が spec の module 数との一致を要求する)。**「宣言は `opencv_unity_native.h` に在る」と書いた記述は、この時点で誤りになった。** +公開 ABI の内訳は次のとおり(**本数を数えるのは `docs/api-map.md` の冒頭だけ**。ここは何が在るかを説明する)。M0/M1 由来の 8 本(`ocvu_get_abi_version`、last-error の取得、status 表の照会、`ocvu_get_opencv_version` / `ocvu_get_build_information`)に、M2 で `Mat` のライフサイクルと buffer 転送の 6 本(`ocvu_mat_create` / `_release` / `_clone` / `_get_info` / `_copy_from_buffer` / `_copy_to_buffer`。`native/src/ocvu_mat_table.cpp`、`native/src/ocvu_mat.cpp`、`native/src/ocvu_mat_buffer.cpp`)、`imgproc` の 3 本(`ocvu_cvt_color` / `ocvu_resize` / `ocvu_gaussian_blur`。`native/src/ocvu_imgproc.cpp`)、L3 のクラッシュ・ハング耐性を実証する `ocvu_debug_crash`(`native/src/ocvu_debug.cpp`)が加わり、M3.5 で `imgcodecs` の 2 本(`ocvu_imencode` / `ocvu_imdecode`。`native/src/ocvu_imgcodecs.cpp`)、続く M5 の module 拡張で `objdetect` の 2 本(`ocvu_qr_encode` / `ocvu_qr_decode`。`native/src/ocvu_objdetect.cpp`)と `features` の 1 本(`ocvu_orb_detect`。`native/src/ocvu_features.cpp`)、`geometry` の 1 本(`ocvu_find_homography`。`native/src/ocvu_geometry.cpp`)、続けてカメラの歪み補正の 2 本(`ocvu_undistort` / `ocvu_find_chessboard_corners`。**`imgproc` と `objdetect` にまたがるが、実装は `native/src/ocvu_calibration.cpp` の 1 ファイルにまとめてある** —— `calib` module は使っていない)が加わった。**2026-09 の API 拡張で 26 本足した** —— `geometry` の姿勢 4 本と `ocvu_get_perspective_transform`(`native/src/ocvu_pose.cpp` と `ocvu_geometry.cpp`)、`objdetect` の ArUco 2 本(`native/src/ocvu_aruco.cpp`)、`imgproc` の 8 本(`native/src/ocvu_imgproc_ops.cpp` と `ocvu_imgproc_shape.cpp`)、`core` の基本演算 8 本(`native/src/ocvu_core_ops.cpp`)、`features` の記述子 2 本(`native/src/ocvu_matching.cpp`)、**そして新設した `stereo` module の 1 本**(`native/src/ocvu_stereo.cpp`)である。**`stereo` は 8 つ目のリンク済み module だが、`tools/opencv-config.psd1` の `Modules` は触っていない** —— `calib` が推移的に引くので OpenCV 側は既にビルドしており、`COMPONENTS` に 1 語足すだけで済んだ(構成ハッシュは変わらず、6 platform 分の再ビルドは起きていない)。**M7c で `dnn` を 9 つ目のリンク済み module として足した**(下の M7c の段落参照)。所有権・versioning・API allowlist の正本は `docs/abi-ownership-and-versioning.md`。**M5 で宣言の在り処が変わった** —— `extern "C"` の関数宣言は `native/include/opencv_unity_native.h` から module ごとの `native/include/ocvu/{infra,core,imgproc,imgcodecs,objdetect,features,geometry,calib,stereo,dnn}.h` へ移り、**いずれも生成物になった**。もとのヘッダが今も持つのは `OCVU_STATUS_LIST`・handle と struct の型・`OCVU_*` 定数だけで、**関数宣言はもう 1 本も無い**(10 個を include するだけである。**その include の行だけは生成物ではない** —— `tools/tests/BindingGenerator.Tests.ps1` が spec の module 数との一致を要求する)。**「宣言は `opencv_unity_native.h` に在る」と書いた記述は、この時点で誤りになった。** + +**M7c(2026-09-08〜10)で `dnn` を opt-in profile として足した。** `bindings/spec/dnn.json` が `"profile": "dnn"` を宣言する初めての module で、C ABI 4 本(`ocvu_dnn_net_read_onnx` / `_net_release` / `_blob_from_image` / `_net_forward`。`native/src/ocvu_dnn.cpp` と handle table の `native/src/ocvu_dnn_table.cpp`)が加わった。**native binary は既定で `dnn` を含む** —— `tools/opencv-config.psd1` の `Modules` に `dnn` を足し(構成ハッシュが変わり 6 platform 分の OpenCV を作り直した)、`cmake/FindOpenCvUnityDeps.cmake` の `COMPONENTS` にも足したので、配る binary は 1 つのままで `dnn` の関数も必ず入っている。**切るのは C# 側だけである** —— 生成された P/Invoke 宣言は別 assembly(`CvUnity.Interop.Dnn` の `NativeMethodsDnn`、`Runtime/Interop.Dnn/`)へ出て、`defineConstraints: ["OCVU_PROFILE_DNN"]` の下でしかコンパイルされない。**利用者が opt-in する経路は Player Settings の Scripting Define Symbols に `OCVU_PROFILE_DNN` を足すことで**、`README.md` / `README.ja.md` に書いてある。**`OCVU_ABI_VERSION` は 1 のままである** —— profile は「どの宣言がコンパイルされるか」を変えるだけで、境界の契約の版は変えない(`docs/abi-ownership-and-versioning.md` §4)。**dnn を足したことで全部入り tarball が配布上限(100 MB)を実際に超えた**(PR #72、`package が上限を超えた: 124101923 > 104857600`)が、**原因は dnn ではなく Android の `.so` に前から積もっていた DWARF デバッグ情報**で、`native/CMakeLists.txt` に Android 限定の strip を足して解消した(詳細と実測は `docs/roadmap.md` の「dnn の配布の形」)。**M7 完了条件 1(profile ごとの native artifact、manifest、third-party notices)の判定は `docs/roadmap.md` の `### M7c の判定` にある** —— 実機で動かしたことは無く、third-party のライセンス面にも未解決の穴が残る。 **この 2 本はファイルパスを受け取らない。メモリ上の byte 列だけを扱う。**「画像ファイルの読み書き」ではない。理由は 2 つ: Windows では境界を越えるパスの文字コードが問題になること、Android では StreamingAssets が APK の中にあってパスでは開けないこと。拡張子(`".png"` 等)も marshal した文字列ではなく、**UTF-8 の NUL 終端 byte 列として明示的に渡す**。また `ocvu_imencode` は **出力の大きさを呼ぶ側が事前に知り得ない最初の ABI 関数**である。native 側が確保した blob の handle は導入せず、既存の 2 回呼びの作法(`OCVU_STATUS_BUFFER_TOO_SMALL` + `out_required_size`)に載せた —— byte 列の所有権は最初から最後まで呼ぶ側にある。buffer が足りないときは何も書かない。C# 側の入口は `CvCodecs`(`Runtime/Core/CvCodecs.cs`)で、`CvOps` には入れていない(あちらは `imgproc` の範囲である)。 @@ -65,13 +67,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | コマンド | 内容 | 実測 | | --- | --- | --- | | `./tools/dev.ps1 build` | native の configure + build | — | -| `./tools/dev.ps1 generate` | **M5。** `bindings/spec/*.json` から C ヘッダ・C# の P/Invoke・到達性テスト・API 対応表を書き出す(**20 ファイル**)。**境界の宣言を手で書く経路はもう無い** | — | +| `./tools/dev.ps1 generate` | **M5。** `bindings/spec/*.json` から C ヘッダ・C# の P/Invoke・到達性テスト・API 対応表を書き出す(**23 ファイル**。M7c で `dnn` が加わり 20 → 23 になった —— ヘッダ 9→10、`NativeMethods.*.g.cs` 9→10、到達性テスト 1→2、`docs/api-map.md` は 1 のまま)。**境界の宣言を手で書く経路はもう無い** | — | | `./tools/dev.ps1 verify-generated` | **M5。** 生成物が spec と一致しているかだけを見る(書き直さない)。食い違えば差分を並べて非 0 で返る。`test` に入っている | — | | `./tools/dev.ps1 test` | **既定**。tools の速いテスト + `verify-generated` + L1 + L3 | **約 1 分 32 秒**(増分、成果物が最新。2026-09-07 実測、exit 0。同じ条件で 2 回測って 93.3 秒 / 91.9 秒)。**M3.5 以前の「約 21 秒」→ 2026-09-01 の「約 65 秒」→ 2026-09-06 の「約 2 分 24 秒」→ 2026-09-07 前半の「約 2 分 13 秒」(`dotnet build` を伴う検査を slow lane へ切り出しただけの値)から、この行の値まで縮んだ。** 2026-09-06 の回帰は 2 つの独立な原因が重なっていた: (1) `BindingGenerator.Tests.ps1` の `dotnet build` 検査(単体 36 秒)—— `tools/tests/NonStandardProfileCompile.Tests.ps1` へ切り出し `$ToolsTestScriptsSlow`(CI 専用)へ移した。(2) **こちらが主因だった**: 同じファイルの負の対照が `native/include/ocvu/*.h` 等の生成物をバイト単位で元へ戻しつつ、**その `LastWriteTime` は戻していなかった**。CMake が configure し直した `.vcxproj` を MSBuild が見ると、依存先ヘッダの mtime が新しいというだけで `native/src/*.cpp`(20 本超)全部を再コンパイル対象と判定する。実測: 直したことで `test-tools` 直後の `test-native` 単体が 56 秒 → 9 秒に戻った。**復元は `verify-generated`(spec からの独立な再生成と比較)が成功したときだけ mtime を戻す**——自分が書いた変数を自分で読み比べる形は同語反復(渡す変数を取り違えても常に一致する)になるので採らなかった。負の対照(わざと違う内容で「復元」し、mtime に触れないことを見る)も実測済み | | `./tools/dev.ps1 test-tools` | `tools/tests/` の速いレーン(OpenCV 構成・ハッシュ無効化・**生成物と spec の一致**・**Unity と Emscripten の対応表**・**tarball の大きさ**・**binary の公開面が spec と一致すること(M7b)**・**benchmark の結果が publish できる形かどうか**)。**本数を写さない。正本は `tools/dev.ps1` の `$ToolsTestScriptsFast` である**(M7 で 2 本増えた。この列挙もそれに合わせて足した——列挙自体を数の代わりにしない) | 約 18 秒(**2 本だった頃の値**)。**2026-09-07 実測では約 75 秒**(74.3 秒 / 74.6 秒 / 75.4 秒、3 回測って安定)——`dotnet build` を伴う検査を抜いたあと、`BindingGenerator.Tests.ps1` の負の対照に mtime 復元の正負両対照を足した後の値。**時間そのものより、この直後に native をビルドしても再コンパイルを誘発しないことのほうが効いている**(下の `test` 行) | | `./tools/dev.ps1 test-tools-slow` | **CI 専用**。allowlist 検証・restore の実 download・linkage 検証・配布物生成 | 約 4 分 15 秒(2026-08-28 実測。M3 で `VerifyArtifactLinkage.Tests.ps1` と `PackageRelease.Tests.ps1` が加わり、M1 時点の約 70 秒から伸びた)。**M7b で `NonStandardProfileCompile.Tests.ps1`(`BindingGenerator.Tests.ps1` から切り出した `dotnet build` 検査)が加わり、約 4 分 34 秒になった**(2026-09-07 実測、exit 0、186 assertion PASS) | -| `./tools/dev.ps1 test-native` | L1 のみ(GoogleTest **215 件** + CTest **4 件**) | 約 10 秒(**`test-tools` の直後でも同じ**。2026-09-07 実測、9.0 秒 / 8.9 秒 / 8.8 秒。M7b で mtime 復元を直す前は、直前に `test-tools` を走らせると 56〜57 秒に伸びていた——ヘッダの mtime が新しいままだったため) | -| `./tools/dev.ps1 test-managed` | L3 のみ。**solution の全テストプロジェクトを回す** —— `CvUnity.Tests.Managed` が P/Invoke 越しに実物の DLL を叩く **185 件**(M7a で `AllocationTests` が加わり 181 → 185)、`Ocvu.Generator.Tests` が spec と生成器を見る **116 件**(M5 で新設、106 件。M7b で profile 分岐のテストが加わり 113 件、最終レビューの C-1 で `LibraryName` の写しを見る 3 件が加わり 116 件になった)。**件数を書いてあるのはこの行だけである** —— 他所に写すと、テストを 1 件足した瞬間にそちらだけが嘘になる(M5 で実際に 4 箇所が同時に古くなった) | 約 11 秒(**44 件だけだった頃の値**) | +| `./tools/dev.ps1 test-native` | L1 のみ(GoogleTest **234 件**(M7c で `dnn` の L1 が加わり 215 → 234 になった) + CTest **4 件**) | 約 10 秒(**`test-tools` の直後でも同じ**。2026-09-07 実測、9.0 秒 / 8.9 秒 / 8.8 秒。M7b で mtime 復元を直す前は、直前に `test-tools` を走らせると 56〜57 秒に伸びていた——ヘッダの mtime が新しいままだったため) | +| `./tools/dev.ps1 test-managed` | L3 のみ。**solution の全テストプロジェクトを回す** —— `CvUnity.Tests.Managed` が P/Invoke 越しに実物の DLL を叩く **196 件**(M7a で `AllocationTests` が加わり 181 → 185、M7c で `DnnInferenceTests` が加わり 185 → 195、レビューの M6 で forward の OpenCV エラー経路を実証するテストが 1 件加わり 196 になった)、`Ocvu.Generator.Tests` が spec と生成器を見る **116 件**(M5 で新設、106 件。M7b で profile 分岐のテストが加わり 113 件、最終レビューの C-1 で `LibraryName` の写しを見る 3 件が加わり 116 件になった)。**件数を書いてあるのはこの行だけである** —— 他所に写すと、テストを 1 件足した瞬間にそちらだけが嘘になる(M5 で実際に 4 箇所が同時に古くなった) | 約 11 秒(**44 件だけだった頃の値**) | | `./tools/dev.ps1 test-asan` | L2(AddressSanitizer) | 約 18 秒(増分。2026-08-28 実測。M1 時点の約 11 秒より伸びているが、原因は未特定——増えたのは L1 側で計測済みの再コンパイル対象と同じファイル群で、M3 固有の変更ではない) | | `./tools/dev.ps1 test-managed-probe` | **CI 専用**。L3 のクラッシュ・ハングプローブ | 約 50 秒(segfault 6 秒 + hang 36 秒) | | `./tools/dev.ps1 test-unity-editmode` | L4(Unity EditMode、Mono、**58 件**。`-testCategory '!Graphics'` で `test-unity-graphics` 側のテストを除外する) | 約 27 秒(増分。2026-08-28 実測。**Unity 6000.0.82f1・EditMode 10 件のときの値。** その後 M3.5 で 16 件、M4 で 33 件、M5 で 34 件、2026-09 の API 拡張で 48 件、M7a で 53 件、M7b で `ProfileGatingTests`(4 件)が加わり 57 件になったが所要時間は取り直していない) | @@ -110,17 +112,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | 場所 | 内容 | | --- | --- | -| `bindings/spec/*.json` | **境界の正本(M5)。** module ごとに 1 ファイル(`infra` / `core` / `imgproc` / `imgcodecs` / `objdetect` / `features` / `geometry` / `calib` / `stereo`)。`schema.json` が形を縛る。**entry 数を数えるのは `docs/api-map.md` の冒頭だけ**である。**ここに 1 エントリ書いて `dev.ps1 generate` を実行するのが、関数を足す唯一の経路である** | +| `bindings/spec/*.json` | **境界の正本(M5)。** module ごとに 1 ファイル(`infra` / `core` / `imgproc` / `imgcodecs` / `objdetect` / `features` / `geometry` / `calib` / `stereo` / `dnn`)。`dnn` は `"profile": "dnn"` を持つ初めての module(M7c)。`schema.json` が形を縛る。**entry 数を数えるのは `docs/api-map.md` の冒頭だけ**である。**ここに 1 エントリ書いて `dev.ps1 generate` を実行するのが、関数を足す唯一の経路である** | | `bindings/generator/` | spec を読んで生成物を書く net8.0 の console(`Ocvu.Generator`)と、その xUnit(`Ocvu.Generator.Tests`)。`tests/Managed/CvUnity.Managed.sln` に同居するので `dev.ps1 test-managed` が一緒に回す(**件数はその行にある**) | -| `native/include/opencv_unity_native.h` | 公開 C ABI ヘッダの入口。`OCVU_STATUS_LIST`(status code の唯一の定義元)・handle と struct の型・`OCVU_*` 定数を持ち、下の 9 つを include する。**M5 以降、関数宣言はここに 1 本も無い**(足しても次の `generate` で消える) | -| `native/include/ocvu/{infra,core,imgproc,imgcodecs,objdetect,features,geometry,calib,stereo}.h` | **生成物(M5)。** module ごとの `extern "C"` 宣言。**手で編集しない** —— `dev.ps1 verify-generated` が落とす | +| `native/include/opencv_unity_native.h` | 公開 C ABI ヘッダの入口。`OCVU_STATUS_LIST`(status code の唯一の定義元)・handle と struct の型・`OCVU_*` 定数を持ち、下の 10 個を include する。**M5 以降、関数宣言はここに 1 本も無い**(足しても次の `generate` で消える) | +| `native/include/ocvu/{infra,core,imgproc,imgcodecs,objdetect,features,geometry,calib,stereo,dnn}.h` | **生成物(M5)。** module ごとの `extern "C"` 宣言。**手で編集しない** —— `dev.ps1 verify-generated` が落とす | | `docs/api-map.md` | **生成物(M5)。** spec の entry を 1 行ずつ並べた API 対応表。**本数を数えるのはこの表の冒頭だけ**で、他所に数字を写さない。「OpenCV 全対応」のような曖昧な表現を置かないための出口でもある | -| `native/src/` | C ABI 実装(version / last-error / status 表 / debug throw・crash / OpenCV version・build information / `Mat` のライフサイクルと buffer 転送 / imgproc 3 関数 / **imgcodecs の encode・decode 2 関数** / objdetect の QR 符号化・復号 2 関数 / features の ORB 検出 1 関数 / geometry の射影変換推定 1 関数 / **カメラ校正 3 関数(`native/src/ocvu_calibration.cpp`。`objdetect` の `ocvu_find_chessboard_corners`・`calib` の `ocvu_calibrate_camera`・`imgproc` の `ocvu_undistort` が 1 ファイルに同居する ── 用途は 1 つだが 3 module にまたがるため)**、**2026-09 の API 拡張の 7 ファイル(`ocvu_pose.cpp` 姿勢 4 本 / `ocvu_aruco.cpp` ArUco 2 本 / `ocvu_imgproc_ops.cpp` 画素を作る 5 本 / `ocvu_imgproc_shape.cpp` 形を返す 3 本 / `ocvu_core_ops.cpp` 基本演算 8 本 / `ocvu_matching.cpp` 記述子 2 本 / `ocvu_stereo.cpp` 視差 1 本)**) | +| `native/src/` | C ABI 実装(version / last-error / status 表 / debug throw・crash / OpenCV version・build information / `Mat` のライフサイクルと buffer 転送 / imgproc 3 関数 / **imgcodecs の encode・decode 2 関数** / objdetect の QR 符号化・復号 2 関数 / features の ORB 検出 1 関数 / geometry の射影変換推定 1 関数 / **カメラ校正 3 関数(`native/src/ocvu_calibration.cpp`。`objdetect` の `ocvu_find_chessboard_corners`・`calib` の `ocvu_calibrate_camera`・`imgproc` の `ocvu_undistort` が 1 ファイルに同居する ── 用途は 1 つだが 3 module にまたがるため)**、**2026-09 の API 拡張の 7 ファイル(`ocvu_pose.cpp` 姿勢 4 本 / `ocvu_aruco.cpp` ArUco 2 本 / `ocvu_imgproc_ops.cpp` 画素を作る 5 本 / `ocvu_imgproc_shape.cpp` 形を返す 3 本 / `ocvu_core_ops.cpp` 基本演算 8 本 / `ocvu_matching.cpp` 記述子 2 本 / `ocvu_stereo.cpp` 視差 1 本)**、**dnn の 4 関数(M7c。`ocvu_dnn.cpp` に ONNX 読み込み・blob 化・forward、handle table は `ocvu_dnn_table.cpp` に分けてある)**) | | `native/tests/` | L1 の GoogleTest と、意図的にクラッシュ・ハングする `ocvu_probe` | | `cmake/run_expect_failure.cmake` | 「失敗するはずのコマンド」を走らせる CTest ドライバ | -| `cmake/FindOpenCvUnityDeps.cmake` | `third_party/opencv//` を探して OpenCV を取り込む。**`COMPONENTS` の行がこのプラグインの実際のリンク対象を決める**(現在は `core imgproc imgcodecs objdetect features geometry calib stereo`。**`flann` と `geometry` は `features` / `objdetect` の依存として推移的に引かれるので、書かなくてもリンク行に入る** —— 明示は意図の宣言であって、no-op であることは実測で確かめてある。**対して `calib` は違う** —— どの module からも引かれないので、この 1 行が実際にリンク行を変える。足す前は `cv::calibrateCamera` が未解決の外部シンボルになった)—— `tools/opencv-config.psd1` の `Modules` は OpenCV 側が何をビルドするかであって、こちらが何をリンクするかではない | -| `native/modules.cmake` | **正本(M7b)。** `OCVU_MODULES` に渡した module の一覧から `OCVU_SOURCES` を組み立てる —— **このプラグイン自身が何をビルドするか**を決める。**同じ「module」という言葉が指す 3 つは別物である**: `tools/opencv-config.psd1` の `Modules` は OpenCV 側が何をビルドするか、`cmake/FindOpenCvUnityDeps.cmake` の `COMPONENTS` はこのプラグインが何をリンクするか、この `OCVU_MODULES` はこのプラグインが何をビルドするか。既定は `OCVU_ALL_MODULES`(全 9 module)。`infra` と `core` は他の全 module が使うので外せない(`OCVU_REQUIRED_MODULES`。外そうとすると configure が `FATAL_ERROR` で落ちる)。**`-DOCVU_MODULES=...` は CMake キャッシュに sticky である** —— 一度絞ると `-UOCVU_MODULES` で明示的に外すかビルド木を作り直すまで既定へ戻らないので、configure のたびに実際の一覧を `message`(既定でないときは `WARNING`)で可視化する。**CMake target は今も 1 つのまま**である —— ここが組み立てるのは**ソースの一覧**であって、コンパイル済みの中間物ではない。`native/CMakeLists.txt` は同じソースを `opencv_unity_native`(`OCVU_BUILDING_DLL`)と `ocvu_static`(L1 テスト用、`OCVU_STATIC` を PUBLIC で持つ)へ **2 回コンパイルする**ので、1 度だけコンパイルして両方へ配る形は取れない。profile との関係は `docs/abi-ownership-and-versioning.md` §4 が正本 | -| `tools/opencv-config.psd1` | OpenCV ビルド構成の唯一の定義元。**`Modules` は現在 `core imgproc imgcodecs objdetect features calib` の 6 つ**(**`stereo` は入っていない** —— `calib` が推移的に引くので OpenCV 側は既にビルドしており、`COMPONENTS` に足すだけで済んだ。構成ハッシュは変わっていない)(`calib` は 2026-09-02 に足した。構成ハッシュが変わるので、触ると**全 platform 分**の再ビルドになる —— **数を写さない。正本は同ファイルの `Toolchains`**)。`Toolchains`(platform 別 generator/architecture)と `PlatformCMakeArgs`(platform 固有 flag)+ 共通の module allowlist・CMake flags(M3 Task 1 で platform 別化) | +| `cmake/FindOpenCvUnityDeps.cmake` | `third_party/opencv//` を探して OpenCV を取り込む。**`COMPONENTS` の行がこのプラグインの実際のリンク対象を決める**(現在は `core imgproc imgcodecs objdetect features geometry calib stereo dnn`。**`flann` と `geometry` は `features` / `objdetect` の依存として推移的に引かれるので、書かなくてもリンク行に入る** —— 明示は意図の宣言であって、no-op であることは実測で確かめてある。**対して `calib` は違う** —— どの module からも引かれないので、この 1 行が実際にリンク行を変える。足す前は `cv::calibrateCamera` が未解決の外部シンボルになった)—— `tools/opencv-config.psd1` の `Modules` は OpenCV 側が何をビルドするかであって、こちらが何をリンクするかではない | +| `native/modules.cmake` | **正本(M7b)。** `OCVU_MODULES` に渡した module の一覧から `OCVU_SOURCES` を組み立てる —— **このプラグイン自身が何をビルドするか**を決める。**同じ「module」という言葉が指す 3 つは別物である**: `tools/opencv-config.psd1` の `Modules` は OpenCV 側が何をビルドするか、`cmake/FindOpenCvUnityDeps.cmake` の `COMPONENTS` はこのプラグインが何をリンクするか、この `OCVU_MODULES` はこのプラグインが何をビルドするか。既定は `OCVU_ALL_MODULES`(全 10 module。M7c で `dnn` が加わった)。`infra` と `core` は他の全 module が使うので外せない(`OCVU_REQUIRED_MODULES`。外そうとすると configure が `FATAL_ERROR` で落ちる)。**`-DOCVU_MODULES=...` は CMake キャッシュに sticky である** —— 一度絞ると `-UOCVU_MODULES` で明示的に外すかビルド木を作り直すまで既定へ戻らないので、configure のたびに実際の一覧を `message`(既定でないときは `WARNING`)で可視化する。**CMake target は今も 1 つのまま**である —— ここが組み立てるのは**ソースの一覧**であって、コンパイル済みの中間物ではない。`native/CMakeLists.txt` は同じソースを `opencv_unity_native`(`OCVU_BUILDING_DLL`)と `ocvu_static`(L1 テスト用、`OCVU_STATIC` を PUBLIC で持つ)へ **2 回コンパイルする**ので、1 度だけコンパイルして両方へ配る形は取れない。profile との関係は `docs/abi-ownership-and-versioning.md` §4 が正本 | +| `tools/opencv-config.psd1` | OpenCV ビルド構成の唯一の定義元。**`Modules` は現在 `core imgproc imgcodecs objdetect features calib dnn` の 7 つ**(**`stereo` は入っていない** —— `calib` が推移的に引くので OpenCV 側は既にビルドしており、`COMPONENTS` に足すだけで済んだ。構成ハッシュは変わっていない)(`calib` は 2026-09-02 に、`dnn` は M7c で足した。どちらも構成ハッシュが変わるので、触ると**全 platform 分**の再ビルドになる —— **数を写さない。正本は同ファイルの `Toolchains`**)。`Toolchains`(platform 別 generator/architecture)と `PlatformCMakeArgs`(platform 固有 flag)+ 共通の module allowlist・CMake flags(M3 Task 1 で platform 別化) | | `tools/OpenCvConfig.psm1` | 構成の読み込みと構成ハッシュの算出(`opencv.ps1` と CI の両方が使う)。`Get-OpenCvPlatform` が `$IsWindows`/`$IsMacOS`/`$IsLinux` から実行中の platform を判定する | | `tools/opencv.ps1` | OpenCV の `restore` / `build` / `verify` / `status` / `clean` の入口 | | `tools/verify-opencv-artifact.ps1` | ビルド済み OpenCV ツリーに対する依存 allowlist の検証(denylist ではない) | @@ -153,13 +155,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | `third_party/opencv//` | 展開先(gitignore 済み)。`build-manifest.json` に実測の構成が入る | | `THIRD_PARTY_NOTICES.md` | OpenCV が bundle する third-party のライセンス全文。**構成ハッシュを埋め込まない** — パスは `` 表記で、取得方法を文書内に書いてある(値を書くと構成を変えるたびに古くなる。M3 で 19 箇所が一斉に死んだ)。`package-release.ps1` が配布物に同梱する | | `Packages/com.ayutaz.opencv-unity-native/Runtime/Interop/` | P/Invoke の置き場。**宣言は `NativeMethods..g.cs`(生成物)にあり、手書きの `[DllImport]` は 1 個も無い**(M5。本数は `docs/api-map.md` が数える)。手で残してある `NativeMethods.cs` が持つのは `LibraryName` と、spec が表現しない struct 3 つ(`OcvuMatInfo` / `OcvuKeyPoint` / `OcvuDMatch`)だけである | -| `Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/` | **非既定 profile(`dnn`)の宣言先(M7b)。** spec の `profile` が `dnn` の module があれば、生成物 `NativeMethods..g.cs` はここへ、クラス名は `NativeMethodsDnn`、assembly は `CvUnity.Interop.Dnn` になる(規約は `docs/abi-ownership-and-versioning.md` §4)。asmdef は `defineConstraints: ["OCVU_PROFILE_DNN"]` を持ち、既定ビルドには含まれない。**commit された状態では中身が空である** —— `bindings/spec/dnn.json` がまだ無く、`profile: "dnn"` の module も無い。define を立てて Unity 自身に「実際にコンパイルされ、外すと消えるか」を問う検査は `tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs` にある。**生成物が実際にコンパイルできることは 2026-09-06 まで誰も確かめていなかった** —— 確かめられていたのは「`defineConstraints` が**手書きの**コードを切る」ことまでで、その穴を通って欠陥が 1 件入っていた(生成物の `[DllImport(LibraryName, ...)]` が `LibraryName` を見られず CS0103。**非既定 profile は別 assembly・別型なので、手書きの `NativeMethods.cs` にある `internal const` が見えない**)。いまは生成器がその 5 行を `#if` 分岐ごと複製し、写し 2 つを `Ocvu.Generator.Tests` の `ProfileTests` が読み比べ、**生成物そのものをコンパイルする検査**が速いレーン(`tools/tests/BindingGenerator.Tests.ps1`)に在る。切り出さなかった理由は `docs/abi-ownership-and-versioning.md` §4 | +| `Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/` | **非既定 profile(`dnn`)の宣言先(M7b で機構を作り、M7c で初めて中身が入った)。** spec の `profile` が `dnn` の module があれば、生成物 `NativeMethods..g.cs` はここへ、クラス名は `NativeMethodsDnn`、assembly は `CvUnity.Interop.Dnn` になる(規約は `docs/abi-ownership-and-versioning.md` §4)。asmdef は `defineConstraints: ["OCVU_PROFILE_DNN"]` を持ち、既定ビルドには含まれない。**M7b の時点では commit された状態で中身が空だった**(`bindings/spec/dnn.json` がまだ無かった)が、**M7c が `bindings/spec/dnn.json` を書いて `NativeMethods.Dnn.g.cs` に実際の 4 関数を出した**——もう空ではない。define を立てて Unity 自身に「実際にコンパイルされ、外すと消えるか」を問う検査は `tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs` にある。**生成物が実際にコンパイルできることは 2026-09-06 まで誰も確かめていなかった** —— 確かめられていたのは「`defineConstraints` が**手書きの**コードを切る」ことまでで、その穴を通って欠陥が 1 件入っていた(生成物の `[DllImport(LibraryName, ...)]` が `LibraryName` を見られず CS0103。**非既定 profile は別 assembly・別型なので、手書きの `NativeMethods.cs` にある `internal const` が見えない**)。いまは生成器がその 5 行を `#if` 分岐ごと複製し、写し 2 つを `Ocvu.Generator.Tests` の `ProfileTests` が読み比べ、**生成物そのものをコンパイルする検査**が速いレーン(`tools/tests/BindingGenerator.Tests.ps1`)に在る。切り出さなかった理由は `docs/abi-ownership-and-versioning.md` §4 | +| `Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/` | **`dnn` の C# 公開 API(M7c)。** `CvDnn.cs`(`CvUnity.Dnn` assembly、`CvUnity.Dnn.asmdef` も `defineConstraints: ["OCVU_PROFILE_DNN"]` を持つ)が `NativeMethodsDnn` の上に立つ。`Runtime/UnityIntegration/` と同じく、profile を切った利用者のビルドからは丸ごと消える | | `Packages/com.ayutaz.opencv-unity-native/` | UPM パッケージ本体(`Runtime/Core`、`Runtime/Interop`、`Runtime/UnityIntegration`)。**`Runtime/Plugins/` は丸ごと成果物で、git は追跡しない**——binary も `.meta` も。`dev.ps1 build` がビルドした platform の binary と `.meta` をここへ置く。**配る正の全部入りでは 全 platform 分(M6 以降は 6 つ)が同居する**(`tools/assemble-plugins.ps1` が重ねる) | | `tools/plugin-meta//` | Plugin Import Settings(`.meta`)の正本。`Runtime/Plugins` を根とした鏡像。**package の中に置くと Unity に消される** — binary の無い platform の `.meta` は「asset の無い孤児」と見なされ、mutable な package では実際に削除される(`test-unity-editmode` を Windows で 1 回走らせるだけで macOS/Linux 分が消えることを実測。M3 のレビュー M4)| | `Packages/com.ayutaz.opencv-unity-native/Runtime/UnityIntegration/` | UnityEngine に依存するコード(`TextureConverter` 等)を置く別 asmdef。`Runtime/Core` / `Runtime/Interop` には置かない | | `Packages/com.ayutaz.opencv-unity-native/Samples~/BasicUsage/` | UPM sample(M3 Task 7)。末尾 `~` のため Unity にインポートされるまでコンパイルされない | | `docs/performance.md` | **M7a。** 測った数字(境界のコピー、`Texture2D`/`RenderTexture` 経路、起動時間)と、測っていないことを書く。**時間は公開するが assert しない**(設計 D1)。`RenderTexture` の同期・非同期 2 経路が run をまたぐと大小が入れ替わる実例を載せ、native texture pointer を実装しないと決めた理由と再評価の条件を書く | -| `docs/api-reference.md` | C ABI と C# 公開 API(`CvMat`/`CvOps`/`CvCodecs`/`CvQrCode`/`CvFeatures`/`CvGeometry`/`CvCalibration`/**`CvAruco`**/**`CvCoreOps`**/**`CvStereo`**/`CvNative`/`TextureConverter`/`WebCamTextureConverter`/**`RenderTextureConverter`**/`NativeArrayExtensions`)のリファレンス(M3 Task 7。M3.5 で `ocvu_imencode` / `ocvu_imdecode` と `CvCodecs` が加わった。M5 で `CvCalibration` が加わった。M7a で `RenderTextureConverter` が加わった) | +| `docs/api-reference.md` | C ABI と C# 公開 API(`CvMat`/`CvOps`/`CvCodecs`/`CvQrCode`/`CvFeatures`/`CvGeometry`/`CvCalibration`/**`CvAruco`**/**`CvCoreOps`**/**`CvStereo`**/`CvNative`/`TextureConverter`/`WebCamTextureConverter`/**`RenderTextureConverter`**/`NativeArrayExtensions`/**`CvDnn`**/**`CvNet`**)のリファレンス(M3 Task 7。M3.5 で `ocvu_imencode` / `ocvu_imdecode` と `CvCodecs` が加わった。M5 で `CvCalibration` が加わった。M7a で `RenderTextureConverter` が加わった。M7c で `CvDnn` / `CvNet`(§2.17)が加わった) | | `tests/Managed/CvUnity.Runtime.Shim/` | netstandard2.1 の shim。UnityEngine 非依存をビルドで強制する | | `tests/Managed/CvUnity.Tests.Managed/` | L3 の xUnit テスト(net8.0)。`HarnessProbeTests.cs` がクラッシュ・ハングプローブを持つ。**同じ solution に `bindings/generator/Ocvu.Generator.Tests` が同居する**ので、`dev.ps1 test-managed` は両方を回す(**件数はその行にある**) —— 結果 XML は `managed-{assembly}.xml` で assembly ごとに分かれる(固定名だと後勝ちで上書きされ、先に走ったほうの失敗が読めなくなる。M5 で実測) | | `tests/UnityProject/` | L4(EditMode)と L5(IL2CPP Player)用の最小 Unity プロジェクト。UPM パッケージは `manifest.json` から `file:../../../Packages/...` でローカル参照する。**この参照はディレクトリであって tarball ではない**ので、tarball の中身が壊れていてもこのレーンは通る——**tarball からの導入は `dev.ps1 test-unity-tarball` が別に見る**(使い捨てのプロジェクトに tarball だけを入れて EditMode を走らせ、16/16 pass を実測済み。M3 でこのレーンを足して初めて「導入できない tarball」が見つかった。**M3.5 で `-PluginSource` が加わり、他 platform の plugin 木を重ねた全部入りも検証できる** —— このマシンでビルドできるのは 1 platform 分だけなので、他 platform は公開済み release から取ってくる)。**Git URL からの導入はこの構成では成立しない**(binary が git の追跡外にあるため) | @@ -216,7 +219,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `docs/superpowers/plans/2026-08-28-m3-desktop-three-platforms.md` — M3 の実装計画(タスク単位、TDD 手順つき) - `docs/superpowers/plans/2026-08-30-m3.5-distribution-shape.md` — M3.5 の実装計画(タスク単位、TDD 手順つき) - `docs/openupm-registration.md` — OpenUPM へ出した登録定義と手順。**2026-08-30 に提出し、受理された**(openupm/openupm PR #6843) -- `docs/abi-ownership-and-versioning.md` — `Mat` と Unity メモリの所有権契約、`OCVU_ABI_VERSION` の versioning 規約、**リンクしている 8 module の** API allowlist の正本 +- `docs/abi-ownership-and-versioning.md` — `Mat` と Unity メモリの所有権契約、`OCVU_ABI_VERSION` の versioning 規約、**リンクしている 9 module の** API allowlist の正本 - `docs/unity-opencv-integration-research-and-plan.md` — 競合調査、アーキテクチャ、ライセンス方針、命名方針 - `docs/native-backend-language-tdd-evaluation.md` — C++ / Rust の評価とテストハーネス設計 - `docs/README.md` — 文書一覧とステータス @@ -354,7 +357,7 @@ M2 以降で確定が必要な残りの事項(計画書 §12 のうち未決 | Windows の compiler / runtime linkage | 実行時ライブラリは共有する形。組み込む開発者が同梱物を選べる状態を保つ | M1 Task 8。上の「確定事項」表 | | `Mat` と Unity メモリの lifetime contract | **借用 handle を作らない。** handle は常に native 所有、Unity の buffer は呼び出し内で完結する借用 | `docs/abi-ownership-and-versioning.md` §1 | | C ABI の versioning / 後方互換 | 単一整数の `OCVU_ABI_VERSION`、C# 側は**完全一致**で検査。bump する変更としない変更を明記 | 同 §2 | -| リンクしている 8 module の API allowlist | M2 の 9 本に、M3.5 / M5 / 2026-09 の API 拡張の追加を足したもの。**関数名をここに並べない** —— 1 本足すたびにこの行だけが古くなる。**内訳は同文書 §3.5〜§3.13、本数は §3 の冒頭が数える** | 同 §3・§3.5〜§3.13 | +| リンクしている 9 module の API allowlist | M2 の 9 本に、M3.5 / M5 / 2026-09 の API 拡張 / M7c(`dnn`)の追加を足したもの。**関数名をここに並べない** —— 1 本足すたびにこの行だけが古くなる。**内訳は同文書 §3.5〜§3.14、本数は §3 の冒頭が数える** | 同 §3・§3.5〜§3.14 | ディレクトリ構成の想定は計画書 §10 にある(`native/`、`bindings/spec|generator|generated-checks/`、`Packages/com.ayutaz.opencv-unity-native/`、`tests/UnityProject/`、`tools/`、`cmake/`、`.github/workflows/`)。このうち `native/`、`Packages/com.ayutaz.opencv-unity-native/`、`tests/Managed/`、`tools/`、`cmake/`、`.github/workflows/` は M0 で実在するようになり、`tests/UnityProject/` は M2 で、`bindings/spec/` と `bindings/generator/` は M5 で実在するようになった。**想定にあった `bindings/generated-checks/` だけは作っていない** —— 生成物の一致検査は `bindings/generator/Ocvu.Generator.Tests`(L3 と同じ solution)と `tools/tests/BindingGenerator.Tests.ps1` が持ち、**既にあるレーンに載せたほうが「どこからも走らない検査」を作らずに済む**ためである。 diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs b/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs new file mode 100644 index 0000000..312ee36 --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; + +// **CvUnity.Dnn(M7c Task 4)は CvMat.Handle を直接読む。** +// +// dnn の C ABI 関数(ocvu_dnn_blob_from_image / ocvu_dnn_net_forward)は +// ocvu_mat_handle を引数に取るので、CvUnity.Dnn の CvDnn はその値を CvMat から +// 取り出して NativeMethodsDnn に渡す必要がある —— CvCodecs や CvCalibration が +// 同じアセンブリの内側から Handle を読むのと同じ理由で、外側の別アセンブリに +// 対してもここで同じアクセスを開ける。 +// +// public にはしない —— handle は実装詳細であり、利用者向けの API ではない +// (Runtime/Interop/AssemblyInfo.cs と同じ方針)。 +// +// **Runtime/Interop/AssemblyInfo.cs は「profile ごとに InternalsVisibleTo を +// 並べる」形を明示的に拒んでいる**(LibraryName は生成器が #if 分岐ごと +// 複製する形で答えた)。ここは逆に、profile(CvUnity.Dnn)を名指しで 1 つ +// 足しており、一見すると同じことを Core 側でやっているように見える。 +// +// **正当化できるのは CvMat.Handle が LibraryName と違う種類の値だからである** +// —— LibraryName は文字列定数で、profile ごとに複製しても値は変わらないので +// 生成器による複製で足りた。対して Handle は生きた値(native 側の状態を +// 指す handle そのもの)で、複製のしようがない。この違いが無ければ、 +// ここも Interop と同じく複製で答えるべきだった。 +// +// **将来 profile が増えても、この行を Interop 側でやってよい理由にしない +// こと。** Interop 側で同じ形が要る事態が来たら、それは「複製できない +// 共通の何か」が新たに見つかったということであり、まず疑うべきは +// Interop 自体を切り出すこと(docs/abi-ownership-and-versioning.md §4)で、 +// この Core の前例を根拠に InternalsVisibleTo を並べ始めることではない。 +[assembly: InternalsVisibleTo("CvUnity.Dnn")] diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs.meta b/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs.meta new file mode 100644 index 0000000..a64cc7c --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 49aeded5739740278823e38660c0459f diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn.meta b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn.meta new file mode 100644 index 0000000..fc93c53 --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5044664de6a143caa677a04002da0737 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvDnn.cs b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvDnn.cs new file mode 100644 index 0000000..ffb1750 --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvDnn.cs @@ -0,0 +1,158 @@ +using System; +using CvUnity.Interop.Dnn; + +namespace CvUnity.Dnn +{ + /// + /// native が所有する dnn ネットワークへの handle を包む。 + /// + /// 寿命の契約は と同じである + /// (docs/abi-ownership-and-versioning.md §1.7)—— + /// handle は常に native 側が持ち、二重の は + /// 落ちない(ocvu_dnn_net_release が OCVU_STATUS_INVALID_HANDLE + /// を返すだけで、C# 側はそれを見ない)。 + /// + public sealed class CvNet : IDisposable + { + private ulong _handle; + + private CvNet(ulong handle) { _handle = handle; } + + internal static CvNet FromHandle(ulong handle) => new CvNet(handle); + + internal ulong Handle + { + get + { + ThrowIfDisposed(); + return _handle; + } + } + + public void Dispose() + { + if (_handle == 0) { return; } + NativeMethodsDnn.ocvu_dnn_net_release(_handle); + _handle = 0; + } + + private void ThrowIfDisposed() + { + if (_handle == 0) { throw new ObjectDisposedException(nameof(CvNet)); } + } + } + + /// + /// OpenCV の dnn モジュールへの入口(ONNX の読み込み、推論の入力の前処理、 + /// 推論の実行)。 + /// + /// + /// **opt-in profile である。** OCVU_PROFILE_DNN を立てたプロジェクトだけが + /// この assembly(CvUnity.Dnn)をコンパイルする。この define が無いと + /// CvUnity.Interop.Dnn も存在しないので、参照を持つこの assembly も + /// 一緒に消える必要がある —— そのため同じ defineConstraints を + /// CvUnity.Dnn.asmdef にも置いてある(docs/abi-ownership-and-versioning.md §4)。 + /// + /// **有効な ONNX を読み込んで実際に推論する経路は、この commit ではまだ + /// 実証していない。** ここで確かめているのは壊れた入力・寿命・所有権で、 + /// 実物の小さなモデルを使った正常系は別のタスクが L3 に足す。 + /// + /// + public static class CvDnn + { + /// mean の要素数。B, G, R の 3 個で固定である。 + private const int MeanLength = 3; + + /// + /// メモリ上の ONNX の byte 列からネットワークを読む。 + /// + /// + /// **ファイルパスは受け取らない**(Windows の文字コードと Android の + /// StreamingAssets のため。 と同じ理由)。 + /// + /// ONNX モデルの byte 列。 + /// が null。 + /// が空。 + /// 読めない byte 列だった。 + public static CvNet ReadOnnx(byte[] data) + { + if (data == null) { throw new ArgumentNullException(nameof(data)); } + if (data.Length == 0) + { + throw new ArgumentException("ONNX の byte 列は空であってはなりません。", nameof(data)); + } + + var status = (CvStatus)NativeMethodsDnn.ocvu_dnn_net_read_onnx( + data, data.Length, out ulong handle); + CvNative.ThrowIfFailed(status); + return CvNet.FromHandle(handle); + } + + /// + /// を推論の入力形式(4 次元の blob。N=1, C, height, width)に + /// 変換して に書く。 + /// + /// + /// が受け取る Mat は 4 次元のまま保持される —— + /// のような 2 次元前提のメンバはこの handle を + /// 例外で拒む。2 次元へ潰すのは の出力側だけである。 + /// + /// 変換する画像。 + /// blob を受け取る Mat。呼び出し前の内容は保持されない。 + /// 画素値に掛ける係数(例: 1/255.0)。 + /// 出力の幅(画素)。1 以上、native の上限以下。 + /// 出力の高さ(画素)。1 以上、native の上限以下。 + /// + /// 各チャンネルから引く値。**B, G, R の順で 3 要素**でなければならない。 + /// native は 3 個の scalar として受け取る固定契約であり、配列ではないので + /// 長さを取り違えて境界の外を読む余地が無い —— この + /// はその契約を C# 側で表すための配列で、渡す前に長さを検証する。 + /// + /// true なら R と B を入れ替える。 + /// true ならアスペクト比を保ったまま中央をトリミングする。 + public static void BlobFromImage( + CvMat src, CvMat dst, double scale, int width, int height, + double[] mean, bool swapRb, bool crop) + { + if (src == null) { throw new ArgumentNullException(nameof(src)); } + if (dst == null) { throw new ArgumentNullException(nameof(dst)); } + if (mean == null) { throw new ArgumentNullException(nameof(mean)); } + if (mean.Length != MeanLength) + { + throw new ArgumentException( + $"mean は B, G, R の {MeanLength} 要素でなければなりません" + + $"(渡されたのは {mean.Length} 要素)。", + nameof(mean)); + } + + var status = (CvStatus)NativeMethodsDnn.ocvu_dnn_blob_from_image( + src.Handle, dst.Handle, scale, width, height, + mean[0], mean[1], mean[2], + swapRb ? 1 : 0, crop ? 1 : 0); + CvNative.ThrowIfFailed(status); + } + + /// + /// に ( が + /// 作った blob)を渡して推論を 1 回走らせ、結果を に書く。 + /// + /// + /// 出力は 2 次元の Mat に潰される(rows × cols)。 は + /// net の内部バッファから独立した新しいメモリのコピーであり、同じ + /// で続けて呼んでも書き換わらない。 + /// + /// が返したネットワーク。 + /// が作った 4 次元の blob。 + /// 結果(2 次元)を受け取る Mat。 + public static void Forward(CvNet net, CvMat input, CvMat output) + { + if (net == null) { throw new ArgumentNullException(nameof(net)); } + if (input == null) { throw new ArgumentNullException(nameof(input)); } + if (output == null) { throw new ArgumentNullException(nameof(output)); } + + var status = (CvStatus)NativeMethodsDnn.ocvu_dnn_net_forward( + net.Handle, input.Handle, output.Handle); + CvNative.ThrowIfFailed(status); + } + } +} diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvDnn.cs.meta b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvDnn.cs.meta new file mode 100644 index 0000000..245bce1 --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvDnn.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5418fb6037f84f4882bddfaaffa5ed13 diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef new file mode 100644 index 0000000..0707586 --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef @@ -0,0 +1,15 @@ +{ + "name": "CvUnity.Dnn", + "rootNamespace": "CvUnity.Dnn", + "references": [ + "CvUnity.Interop.Dnn", + "CvUnity.Core" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "noEngineReferences": true, + "defineConstraints": [ + "OCVU_PROFILE_DNN" + ] +} diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef.meta b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef.meta new file mode 100644 index 0000000..05dae7b --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 98b31db2a8a54f8eb27edd9e2f84bc20 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/AssemblyInfo.cs b/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/AssemblyInfo.cs index cc5ba69..bb517df 100644 --- a/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/AssemblyInfo.cs +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/AssemblyInfo.cs @@ -3,9 +3,22 @@ // **到達性テストだけが例外である。** 理由は Runtime/Interop/AssemblyInfo.cs の // 末尾にあるものと同じで、公開 API 経由では「どの entry point が呼ばれたか」を // spec から機械的に導けない。**profile が変わっても、その理由は変わらない。** -// -// **新しい例外を作っているのではなく、既にある例外を同じ理由で profile 側にも -// 置いている。** Runtime/Interop/AssemblyInfo.cs の「Dnn には InternalsVisibleTo を -// 出さない」は CvUnity.Dnn / CvUnity.Runtime / CvUnity.Tests.Managed の 3 つに -// ついての決定であって、この 1 本は含まない。 [assembly: InternalsVisibleTo("CvUnity.Tests.Shared.Dnn")] + +// **M7c Task 4 で、上を「唯一の例外」から「例外の 1 つ」に変えた。** +// +// Runtime/Interop の AssemblyInfo.cs が持つ 3 つの InternalsVisibleTo +// (CvUnity.Core / CvUnity.Tests.Managed / CvUnity.Runtime)を、同じ理由で +// Interop.Dnn 側にも用意する ―― CvUnity.Dnn(Runtime/Dnn/CvDnn.cs)が +// ocvu_net_handle を包む CvNet を実装するには NativeMethodsDnn を直接 +// 呼ぶ必要があり、CvUnity.Interop.Dnn には「Core 層に相当するもの」が +// まだ無いので、公開 API 層自身に internal を見せる。 +// +// public にはしない ―― P/Invoke 宣言は実装詳細であり、利用者向けの API では +// ない(Runtime/Interop/AssemblyInfo.cs と同じ方針)。 +[assembly: InternalsVisibleTo("CvUnity.Dnn")] +[assembly: InternalsVisibleTo("CvUnity.Tests.Managed")] +[assembly: InternalsVisibleTo("CvUnity.Runtime")] + +// 逆向き(Interop が Dnn を見る)は出さない。既定 profile が opt-in profile を +// 知っていたら、切り離せていない。 diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/NativeMethods.Dnn.g.cs b/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/NativeMethods.Dnn.g.cs new file mode 100644 index 0000000..56a3932 --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/NativeMethods.Dnn.g.cs @@ -0,0 +1,34 @@ +// このファイルは生成物である。手で編集しないこと。 +// 正本: bindings/spec/dnn.json +// 生成: ./tools/dev.ps1 generate + +using System.Runtime.InteropServices; + +namespace CvUnity.Interop.Dnn +{ + internal static partial class NativeMethodsDnn + { +#if (UNITY_IOS || UNITY_WEBGL) && !UNITY_EDITOR + internal const string LibraryName = "__Internal"; +#else + internal const string LibraryName = "opencv_unity_native"; +#endif + + /// メモリ上の ONNX の byte 列からネットワークを読み、handle を out_handle に書く。ファイルパスは受け取らない(Windows の文字コードと Android の StreamingAssets のため)。読めなければ OCVU_STATUS_OPENCV_ERROR を返し out_handle は変更しない。data が NULL、out_handle が NULL なら OCVU_STATUS_NULL_POINTER。length が 1 未満なら OCVU_STATUS_INVALID_ARGUMENT。 + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int ocvu_dnn_net_read_onnx(byte[] data, int length, out ulong out_handle); + + /// ocvu_dnn_net_read_onnx が返した handle を解放する。native が所有する(docs/abi-ownership-and-versioning.md §1.7)ので、使い終わったら必ずこれで解放する。net が 0、解放済み、未知の handle のいずれでも OCVU_STATUS_INVALID_HANDLE を返し、落とさない(二重解放を検出する)。 + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int ocvu_dnn_net_release(ulong net); + + /// src を推論の入力形式(4 次元の blob。N=1, C, height, width)に変換して dst に書く。scale は画素値に掛ける係数(例: 1/255.0)。mean_b / mean_g / mean_r は各チャンネルから引く値で、この順(B, G, R)で cv::Scalar に渡す固定契約である —— 配列ではなく 3 個の scalar にしてあるので、長さを取り違えて境界の外を読むことができない。検証の順序は決まっている: まず width / height の範囲(1 以上 OCVU_DNN_MAX_BLOB_DIM 以下。外れれば OCVU_STATUS_INVALID_ARGUMENT。cv::dnn::blobFromImage がその寸法でメモリを確保するため)、次に src の handle、最後に dst の handle(どちらも無効なら OCVU_STATUS_INVALID_HANDLE)。したがって範囲外の width と無効な handle を同時に渡すと OCVU_STATUS_INVALID_ARGUMENT が返る。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数はこの handle を OCVU_STATUS_INVALID_ARGUMENT で拒む)—— 2 次元へ潰すのは ocvu_dnn_net_forward の出力側だけである。OpenCV が例外を投げた場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは dst を書き換えない。 + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int ocvu_dnn_blob_from_image(ulong src, ulong dst, double scale, int width, int height, double mean_b, double mean_g, double mean_r, int swap_rb, int crop); + + /// net に input(ocvu_dnn_blob_from_image が作った blob)を渡して推論を 1 回走らせ、結果を output に書く。出力は 2 次元の Mat に潰される(rows × cols)。4 次元の blob を返すモデル(検出など)では、N と C の区別が失われる。分類モデルの 1 × N の出力を受け取ることを想定している。output は net の内部バッファから独立した新しいメモリのコピーであり、同じ net で続けて forward を呼んでも書き換わらない。handle の検証は net、input、output の順で行い、いずれかが無効なら OCVU_STATUS_INVALID_HANDLE を返す(解放済みの net も同じ status になる)。OpenCV が例外を投げた場合、または結果が空になった場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは output を書き換えない。engine / backend を選ぶ引数は無い(上流の 5.1 で enum EngineType の値が総入れ替えになったため、5.1 で意味が変わる数字を境界の外へ出さない)。 + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl)] + internal static extern int ocvu_dnn_net_forward(ulong net, ulong input, ulong output); + + } +} diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/NativeMethods.Dnn.g.cs.meta b/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/NativeMethods.Dnn.g.cs.meta new file mode 100644 index 0000000..f438cb0 --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/NativeMethods.Dnn.g.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3a03d06d19d7412389e19519bfef5d48 \ No newline at end of file diff --git a/README.ja.md b/README.ja.md index c19ea0a..7971c69 100644 --- a/README.ja.md +++ b/README.ja.md @@ -41,6 +41,8 @@ encode / decode の検査が PNG ではなく JPEG を使うので、**画素の どちらの極端も通らないので、Web のビルドでは PNG を外してあります。 **他の 5 platform は PNG / JPEG の両方を扱えます。** +**PNG のデコードは Android・iOS・macOS でも ARM 加速を失っています**(Web だけではありません)。上流の OpenCV 5.0.0 の欠陥を回避するため、この 3 つの arm64 platform で `PNG_ARM_NEON` を切る必要があり、これはアセンブリで書かれたカーネルと NEON intrinsics の経路を両方とも落とします —— 同じビルドスイッチを共有しているためです。`dnn` を使うかどうかに関わらず、これらの platform で `CvCodecs` を使う利用者全員に影響します。理由の詳細は[ロードマップ](docs/roadmap.md)にあります。 + 全体を通して Unity 6000.3 以降が必要です。 ## 導入 @@ -116,6 +118,25 @@ shasum -a 256 -c SHA256SUMS.txt # macOS 全部入りの tarball を使い捨てのプロジェクトに導入して EditMode を走らせると通ります(このマシンでの実測)。**CI は全 platform の binary を置いた状態で同じ EditMode 群を走らせる**ので、これらの検査が存在する理由である「複数 platform が同居する場合」を、CI がまさに実際に確かめています —— EditMode のテストが、全部揃っているのを見るまで緑になりません。現在の件数やサイズは[ロードマップ](docs/roadmap.md)にあります(リリースのたびに動くので、ここには写しません)。 +### 追加 profile: dnn + +native binary には OpenCV の `dnn` module(ONNX の読み込み、blob 化、forward 推論)が +**常に**入っています —— profile ごとに別 binary へ分けてはいません。理由は、6 platform +のうち iOS と WebGL の 2 つが `DllImport("__Internal")` を**シンボル名**で解決し、 +**ライブラリ名**では解決しないため、切り替える対象の名前がそもそも存在しないからです。 +**任意にできるのは C# 側だけです。** `dnn` の API(`CvDnn`、`CvUnity.Dnn` assembly)は、 +利用側のプロジェクトの **Project Settings → Player → Other Settings → Scripting Define +Symbols** に `OCVU_PROFILE_DNN` を対象 platform 分足したときだけコンパイルされます。 +立てなければ、そのコードと、それが依存する P/Invoke 宣言はビルドに入りません。 + +これは `package.json` の `versionDefines` では自動化できませんでした —— あの機構は +「ある**パッケージ**が入っていれば define を立てる」もので、`dnn` は別パッケージではなく +**同じパッケージの中**にあるため、存在の有無を条件にできる対象がありません。したがって +この define は、パッケージ側が立てるものではなく、**利用者が自分で立てるもの**です。 + +`dnn` は実機で動かしたことがありません(モバイル platform 全般が抱える穴と同じ形です。 +詳細は[ロードマップ](docs/roadmap.md))。推論の速さも測っていません。 + ### リリースの作り方 `v*` の tag を打つと、全 platform をビルドし、ビルドしたものの linkage を検証し、Linux のライブラリが「支える最も古い環境」より新しい glibc や libstdc++ を要求していないかを確かめ、全 platform のプラグイン木を 1 つのパッケージへ重ね、すべてをパッケージ化して、**下書き**のリリースを作ります。glibc の検査はビルド済み `.so` の中の version レコードを読むもので、ライブラリを読み込むわけではありません —— **binary が host に何を要求しているかについての主張**であって、そこで動く証明ではありません。 diff --git a/README.md b/README.md index 8d787ab..ee12d88 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,13 @@ the player fail to link on duplicate symbols, and leaving it out makes OpenCV's fail to link on undefined ones. Neither extreme works, so the Web build has PNG turned off. Every other platform has both. +**PNG decoding lost its ARM acceleration on Android, iOS and macOS** (not just Web). +Working around an upstream OpenCV 5.0.0 defect required turning off `PNG_ARM_NEON` on +those three arm64 platforms, which drops both the hand-written assembly kernel and the +NEON intrinsics path — they share the same build switch. This affects every `CvCodecs` +user on those platforms, whether or not they touch `dnn`. See the +[roadmap](docs/roadmap.md) for why. + Unity 6000.3 or newer throughout. ## Installing @@ -179,6 +186,29 @@ exactly what CI exercises — an EditMode test refuses to go green unless it see all. Current counts and sizes are in the [roadmap](docs/roadmap.md); they move with every release, so they are not repeated here. +### Optional profiles: dnn + +The native binary always contains OpenCV's `dnn` module (ONNX loading, blob +conversion, and forward inference) — it is not split into a separate binary, +because two of the six platforms (iOS, WebGL) resolve `DllImport("__Internal")` +by symbol name, not by library name, so there is no name to switch on. What is +optional is the **C# side**: the `dnn` API (`CvDnn`, in the `CvUnity.Dnn` +assembly) only compiles into your project if you add the Scripting Define +Symbol `OCVU_PROFILE_DNN` under **Project Settings → Player → Other Settings → +Scripting Define Symbols** for the platforms you target. Leave it unset and +that code — and the P/Invoke declarations it depends on — is not part of your +build. + +This could not be automated with `package.json`'s `versionDefines`: that +mechanism fires a define when a given *package* is present, but `dnn` lives +inside this same package rather than a separate one, so there is nothing +whose presence to key off. The define is therefore something you set +yourself, not something the package sets for you. + +`dnn` has not been run on a real device (the same gap the mobile platforms +have generally — see the [roadmap](docs/roadmap.md)), and inference speed has +not been measured. + ### How releases are made Tagging `v*` builds every platform, verifies the linkage of what was built, diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 429e22e..3c5a9db 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -23,17 +23,31 @@ source, which is Apache-2.0 (see [LICENSE](LICENSE)). | macOS / Linux / iOS / Web | `lib/*.a` | `share/licenses/opencv5/` | | Android | `sdk/native/staticlibs/arm64-v8a/*.a` | `sdk/etc/licenses/` | - Modules built for this configuration (`tools/opencv-config.psd1`): - `core`, `imgproc`, `imgcodecs`, `objdetect`, `features`, `calib`, plus `flann`, - `geometry` and `stereo`, pulled in transitively + `core`, `imgproc`, `imgcodecs`, `objdetect`, `features`, `calib`, `dnn`, plus + `flann`, `geometry` and `stereo`, pulled in transitively (`tools/verify-opencv-artifact.ps1` `$AcceptedTransitiveModules`). - **Universe considered**: every file under `third_party/opencv// のライセンスディレクトリ(上表)` in the restored artifact - (`./tools/opencv.ps1 restore`) — **13 files** on the desktop and iOS configurations, **15 on Android** - (`cpufeatures-LICENSE` and `cpufeatures-README.md`, both from the NDK), and - **11 on Web** as of this hash. **Web has two fewer, not more**: it is built with + (`./tools/opencv.ps1 restore`) — counts **as of this hash** (they were last + updated 2026-09-08, when `dnn` added `protobuf-LICENSE` and + `protobuf-README.md` to every platform): **15 on Windows and Linux, 14 on + macOS, 13 on iOS, 16 on Android, 13 on Web.** These are no longer a flat + "desktop and iOS" bucket — that grouping was already an approximation + before this count, and listing each platform actually downloaded and + counted (not restored-then-guessed) surfaced two differences unrelated to + `dnn`: **macOS and iOS both lack `clapack-lapack_LICENSE`** (Windows and + Linux have it; whatever backs linear algebra on Apple Silicon here isn't + bundled CLAPACK), and **iOS additionally lacks `dlpack-LICENSE`**, which + every other platform — including Android and Web — has. Neither of these + is a `dnn` effect (both were true of the pre-`dnn` trees too, as far as + this document's authors can tell from what's on hand); they simply hadn't + been individually counted before. Android's baseline is **cpufeatures-LICENSE + and cpufeatures-README.md** (both from the NDK) on top of the Windows/Linux + set, minus the same missing `clapack-lapack_LICENSE`. **Web has two fewer than + Windows/Linux for an unrelated, already-documented reason**: it is built with `WITH_PNG=OFF`, so `libpng-LICENSE` and `libpng-README` are absent — Unity's own WebGL support ships libpng, and bundling OpenCV's copy makes the player fail to - link on duplicate symbols. Measured 2026-09-03 by listing both trees. This is the + link on duplicate symbols. This is the set OpenCV's own install step attributes as third-party, and it is now the allowlist `tools/verify-opencv-artifact.ps1`'s `$InertLicenseFiles` enforces: a new file appearing there that isn't in that list fails the @@ -816,33 +830,393 @@ POSSIBILITY OF SUCH DAMAGE. --- +## protobuf + +License: BSD 3-Clause (`third_party/opencv// のライセンスディレクトリ(上表)protobuf-LICENSE`) + +Linked into: its own static library alongside the `opencv_*` module +libraries — `libprotobuf.lib` on Windows, `liblibprotobuf.a` on the other +five platforms. **The doubled `lib` prefix is not a typo and not this +repository's naming**: upstream protobuf's own CMake target is itself named +`libprotobuf`, so the platform's usual `lib` prefix is added on top of that +on Unix (confirmed against every platform's build log, run `34212296352`, +2026-09-08). + +**This library is new as of `dnn` being added to `Modules`.** It was not +present, and not linked, in any earlier configuration. OpenCV's ONNX +importer (part of `dnn`) parses models that are serialized with protobuf, +so — unlike the optional codecs this document lists elsewhere — protobuf +is not something this package chose to enable for convenience: without it, +`dnn` does not compile at all (`tools/opencv-config.psd1` used to carry +`WITH_PROTOBUF=OFF` under its "value this project doesn't need" rationale; +that premise stopped holding the moment `dnn` was added, and the comment at +that line explains why rather than silently disappearing). The copy bundled +here is the version OpenCV vendors and builds from source — 3.19.1 — not +whatever protobuf a build machine happens to have installed +(`-DBUILD_PROTOBUF=ON`, for the same reproducibility reason `BUILD_ZLIB` / +`BUILD_PNG` / `BUILD_JPEG` are already `ON` above). + +OpenCV's install also ships `protobuf-README.md` in the same licenses +directory. Like `cpufeatures-README.md` above, it is minimal +project/version metadata (name, upstream URL, bundled version number), not +a separate licence — both files are covered by the BSD-3-Clause text below. +Verified against OpenCV's own vendored copy at the `5.0.0` tag +(), +which is byte-for-byte the same text upstream protobuf ships at its +`v3.19.1` tag. + +**BSD-3-Clause requires the copyright notice and disclaimer to be +reproduced in binary redistributions**, which is what the text below does. + +``` +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. +``` + +--- + +## flatbuffers + +License: Apache License 2.0 +(`third_party/opencv// のライセンスディレクトリ(上表)flatbuffers-LICENSE.txt`) + +Linked into: `opencv_dnn500.lib` (Windows) / the platform-equivalent +`opencv_dnn` static library — the reading side of `dnn`'s TFLite importer +(`opencv_tflite` namespace: `Model`, `SubGraph`, `Operator`, `Tensor` and +friends), which walks a `.tflite` file through +`flatbuffers::Table`, `flatbuffers::VerifierTemplate`, `flatbuffers::Vector`, +`flatbuffers::String`, `flatbuffers::GetRoot` / `GetMutableRoot`, +`flatbuffers::ReadScalar` / `EndianScalar`, and related reader-side +machinery. **`FlatBufferBuilder` (the writer side) was searched for and not +found** — consistent with an importer that only ever reads models, never +serializes them. + +**This library was previously listed below as "present but not linked." +That was correct when last checked, and stopped being correct the moment +`dnn` entered `Modules` (2026-09-08) — TFLite import is part of `dnn`.** +Confirmed by actually restoring a build that has `dnn` (run `34215362804`, +`./tools/opencv.ps1 restore`) and searching its `opencv_dnn500.lib`, not by +inference from compiler flags. + +**Confirmed on two platforms, deliberately chosen for two different +name-mangling schemes** — Windows/MSVC (above) and Linux/Itanium +(`lib/libopencv_dnn.a` from the same run `34215362804`: `flatbuffers` → 164 +matches, `dlpack` → 0 matches, and 0 matches for `dlpack` across every `.a` +in that tree). That second data point matters more than a second platform +would on its own, because the methodology defect described below is +specific to which mangling scheme a toolchain uses — confirming the same +classification under both schemes is what makes it trustworthy, not merely +having checked twice. **The remaining four platforms (macOS, iOS, Android, +Web) were not individually inspected.** They are inferred from building the +same `Modules` list from the same OpenCV source tree, not from grepping +their own `.a` files — record that as inference, not as a third data point. + +**A methodology correction, recorded so the next search doesn't repeat it:** +the two earlier "zero matches" results for this component used patterns +containing `::`, e.g. `flatbuffers::`, mirroring how the identifier reads in +source. **That pattern cannot match on Windows.** MSVC's name-mangling +scheme (`?Verify@ConcatEmbeddingsOptions@opencv_tflite@@...`) does not +preserve `::` as literal text the way Itanium mangling (used by GCC/Clang on +the other five platforms) tends to keep readable substrings — it uses `@` as +a namespace separator instead. Searching for the **bare identifier** +(`flatbuffers`, no punctuation) is what actually finds it: + +``` +grep -a -o -i "flatbuffers" opencv_dnn500.lib | wc -l +``` + +returns several hundred matches, all of them decorated `opencv_tflite` +member-function names of the shape shown above — not the single +`cv::getBuildInformation()` summary-string hit this document previously +noted in `opencv_core500.lib` (that hit is still there, and is still just +build-info text, not code). **This does not retroactively make the earlier +"zero matches" wrong for what they actually tested** — the tree they ran +against had no `dnn`, so there was no flatbuffers-using code to find +regardless of pattern. It does mean the `::`-containing pattern shape is +unreliable on Windows going forward and bare identifiers should be +preferred, especially for any future search on `.lib` (not `.a`) files. + +**Apache License 2.0 requires a copy of the license to accompany +redistribution**, which is what the text below does — the same text OpenCV +itself vendors at `3rdparty/flatbuffers/LICENSE.txt` (verified against the +`5.0.0` tag, +). + +``` + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +``` + +--- + ## Present in `etc/licenses/` but not linked into this build -Two of those files are license texts for components that OpenCV's build -system attributes generally, but a symbol-table search of every `.lib` -under the platform's library directory (see the table above) for identifiers -specific to each found nothing. -Both belong to OpenCV modules (`dnn`, `gapi`) that are **not** in this -configuration's `Modules` list (`tools/opencv-config.psd1`), so their code -was never compiled into anything this package ships. Their license text is -listed here for completeness — so the inventory is fully -accounted for — but is not reproduced, because nothing of theirs ships. +One file remains here — **not two.** `flatbuffers` used to be listed +alongside `dlpack` in this section; it moved up to its own reproduced +section above once `dnn` (which actually uses it, via the TFLite importer) +entered `Modules` and a real build could be searched. See that section for +the full story, including a methodology correction that applies to this +section too. - **dlpack** (Apache License 2.0) — `third_party/opencv// のライセンスディレクトリ(上表)dlpack-LICENSE`. Searched - for `DLManagedTensor`, `DLPackVersioned`, `dlpack` (case-insensitive) - across every `.lib`; zero matches. -- **flatbuffers** (Apache License 2.0) — - `third_party/opencv// のライセンスディレクトリ(上表)flatbuffers-LICENSE.txt`. - Searched for `flatbuffers::`, `FlatBufferBuilder`, `flatbuffers_`; zero - matches. (`opencv_core500.lib` does contain the bare word "Flatbuffers" - once, but only inside the embedded `cv::getBuildInformation()` summary - string — see the note in "Scope of this document" above about why that - doesn't count as linked code.) - -If a future `Modules` list adds `dnn` or `gapi`, re-run these searches — -they will very likely start matching, and these two need to move up into -the reproduced sections above. + the restored `dnn`-enabled tree (run `34215362804`) for the bare + identifiers `DLTensor`, `DLDevice`, `DLDataType`, + `DLManagedTensor`, `dlpack` (case-insensitive, no `::` — see the + methodology note above) across every `.lib` in + `x64/vc17/staticlib/`; zero matches, all patterns, all files. Confirmed a + second time on the Linux artifact from the same run (`dlpack`, zero + matches across every `.a` in that tree) — same two-mangling-scheme + reasoning as the `flatbuffers` entry above. The other four platforms were + not individually inspected; they are inferred from the same `Modules` + list and source tree. + +**Both components were attributed to OpenCV modules (`dnn`, `gapi`) that +were not in this configuration's `Modules` list. That premise is now false +for `dnn`** (added 2026-09-08) **and still true for `gapi`**, which remains +outside `Modules` and outside this build entirely. dlpack staying unlinked +despite `dnn` now being built is consistent with it being a header-only +type-definition library (`DLManagedTensor` et al. are C structs, not +functions) — nothing in this build's compiled sources instantiates them in +a way that leaves a distinct symbol behind, unlike flatbuffers' `Table` / +`Vector` reader templates, which do generate real code. Its license text is +kept here for completeness — so the inventory is fully accounted for — but +is not reproduced, because nothing of it ships. + +If a future `Modules` list adds `gapi`, or if `dlpack` starts matching in a +later search (a genuinely different call site could reference the structs +in a way that survives into object code), re-run the search above and move +it up. --- diff --git a/bindings/generator/Ocvu.Generator/SpecModel.cs b/bindings/generator/Ocvu.Generator/SpecModel.cs index 3a99d49..3e40594 100644 --- a/bindings/generator/Ocvu.Generator/SpecModel.cs +++ b/bindings/generator/Ocvu.Generator/SpecModel.cs @@ -182,6 +182,10 @@ private static ModuleSpec LoadOneFile(string file, SchemaConstraints schemaConst ["uint8_t*"] = new[] { "byte[]", "System.IntPtr" }, ["const char*"] = new[] { "byte[]", "System.IntPtr" }, ["char*"] = new[] { "byte[]", "System.IntPtr" }, + // 読み込んだニューラルネットワークの handle(dnn profile、M7c)。 + // ocvu_mat_handle / ocvu_mat_handle* とまったく同じ形である。 + ["ocvu_net_handle"] = new[] { "ulong" }, + ["ocvu_net_handle*"] = new[] { "out ulong" }, }; private static void RequireTheCsTypeMatchesTheCType( diff --git a/bindings/spec/dnn.json b/bindings/spec/dnn.json new file mode 100644 index 0000000..154f61f --- /dev/null +++ b/bindings/spec/dnn.json @@ -0,0 +1,59 @@ +{ + "module": "dnn", + "profile": "dnn", + "functions": [ + { + "name": "ocvu_dnn_net_read_onnx", + "summary": "メモリ上の ONNX の byte 列からネットワークを読み、handle を out_handle に書く。ファイルパスは受け取らない(Windows の文字コードと Android の StreamingAssets のため)。読めなければ OCVU_STATUS_OPENCV_ERROR を返し out_handle は変更しない。data が NULL、out_handle が NULL なら OCVU_STATUS_NULL_POINTER。length が 1 未満なら OCVU_STATUS_INVALID_ARGUMENT。", + "returns": "ocvu_status", + "csReturns": "int", + "wrapInTryBarrier": true, + "params": [ + { "name": "data", "cType": "const uint8_t*", "csType": "byte[]", "direction": "in-buffer" }, + { "name": "length", "cType": "int32_t", "csType": "int", "direction": "in" }, + { "name": "out_handle", "cType": "ocvu_net_handle*", "csType": "out ulong", "direction": "out" } + ] + }, + { + "name": "ocvu_dnn_net_release", + "summary": "ocvu_dnn_net_read_onnx が返した handle を解放する。native が所有する(docs/abi-ownership-and-versioning.md §1.7)ので、使い終わったら必ずこれで解放する。net が 0、解放済み、未知の handle のいずれでも OCVU_STATUS_INVALID_HANDLE を返し、落とさない(二重解放を検出する)。", + "returns": "ocvu_status", + "csReturns": "int", + "wrapInTryBarrier": true, + "params": [ + { "name": "net", "cType": "ocvu_net_handle", "csType": "ulong", "direction": "in" } + ] + }, + { + "name": "ocvu_dnn_blob_from_image", + "summary": "src を推論の入力形式(4 次元の blob。N=1, C, height, width)に変換して dst に書く。scale は画素値に掛ける係数(例: 1/255.0)。mean_b / mean_g / mean_r は各チャンネルから引く値で、この順(B, G, R)で cv::Scalar に渡す固定契約である —— 配列ではなく 3 個の scalar にしてあるので、長さを取り違えて境界の外を読むことができない。検証の順序は決まっている: まず width / height の範囲(1 以上 OCVU_DNN_MAX_BLOB_DIM 以下。外れれば OCVU_STATUS_INVALID_ARGUMENT。cv::dnn::blobFromImage がその寸法でメモリを確保するため)、次に src の handle、最後に dst の handle(どちらも無効なら OCVU_STATUS_INVALID_HANDLE)。したがって範囲外の width と無効な handle を同時に渡すと OCVU_STATUS_INVALID_ARGUMENT が返る。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数はこの handle を OCVU_STATUS_INVALID_ARGUMENT で拒む)—— 2 次元へ潰すのは ocvu_dnn_net_forward の出力側だけである。OpenCV が例外を投げた場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは dst を書き換えない。", + "returns": "ocvu_status", + "csReturns": "int", + "wrapInTryBarrier": true, + "params": [ + { "name": "src", "cType": "ocvu_mat_handle", "csType": "ulong", "direction": "in" }, + { "name": "dst", "cType": "ocvu_mat_handle", "csType": "ulong", "direction": "in" }, + { "name": "scale", "cType": "double", "csType": "double", "direction": "in" }, + { "name": "width", "cType": "int32_t", "csType": "int", "direction": "in" }, + { "name": "height", "cType": "int32_t", "csType": "int", "direction": "in" }, + { "name": "mean_b", "cType": "double", "csType": "double", "direction": "in" }, + { "name": "mean_g", "cType": "double", "csType": "double", "direction": "in" }, + { "name": "mean_r", "cType": "double", "csType": "double", "direction": "in" }, + { "name": "swap_rb", "cType": "int32_t", "csType": "int", "direction": "in" }, + { "name": "crop", "cType": "int32_t", "csType": "int", "direction": "in" } + ] + }, + { + "name": "ocvu_dnn_net_forward", + "summary": "net に input(ocvu_dnn_blob_from_image が作った blob)を渡して推論を 1 回走らせ、結果を output に書く。出力は 2 次元の Mat に潰される(rows × cols)。4 次元の blob を返すモデル(検出など)では、N と C の区別が失われる。分類モデルの 1 × N の出力を受け取ることを想定している。output は net の内部バッファから独立した新しいメモリのコピーであり、同じ net で続けて forward を呼んでも書き換わらない。handle の検証は net、input、output の順で行い、いずれかが無効なら OCVU_STATUS_INVALID_HANDLE を返す(解放済みの net も同じ status になる)。OpenCV が例外を投げた場合、または結果が空になった場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは output を書き換えない。engine / backend を選ぶ引数は無い(上流の 5.1 で enum EngineType の値が総入れ替えになったため、5.1 で意味が変わる数字を境界の外へ出さない)。", + "returns": "ocvu_status", + "csReturns": "int", + "wrapInTryBarrier": true, + "params": [ + { "name": "net", "cType": "ocvu_net_handle", "csType": "ulong", "direction": "in" }, + { "name": "input", "cType": "ocvu_mat_handle", "csType": "ulong", "direction": "in" }, + { "name": "output", "cType": "ocvu_mat_handle", "csType": "ulong", "direction": "in" } + ] + } + ] +} diff --git a/cmake/FindOpenCvUnityDeps.cmake b/cmake/FindOpenCvUnityDeps.cmake index ae3858e..62a669b 100644 --- a/cmake/FindOpenCvUnityDeps.cmake +++ b/cmake/FindOpenCvUnityDeps.cmake @@ -81,7 +81,7 @@ endforeach() # NO_CMAKE_FIND_ROOT_PATH: この呼び出しに限って sysroot への読み替えを外す。 # PATHS は既に絶対パスで、NO_DEFAULT_PATH が他の経路を閉じている。 find_package(OpenCV ${OCVU_OPENCV_REQUIRED_VERSION} EXACT REQUIRED - COMPONENTS core imgproc imgcodecs objdetect features geometry calib stereo + COMPONENTS core imgproc imgcodecs objdetect features geometry calib stereo dnn NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH PATHS ${OCVU_OPENCV_CONFIG_CANDIDATES}) diff --git a/docs/abi-ownership-and-versioning.md b/docs/abi-ownership-and-versioning.md index c50d844..c126bf5 100644 --- a/docs/abi-ownership-and-versioning.md +++ b/docs/abi-ownership-and-versioning.md @@ -261,6 +261,86 @@ native はそこへ書くだけで、戻った後は一切保持しない。 関数を足しただけなので `OCVU_ABI_VERSION` は 1 のままである(§2「bump しない変更」)。 +## 1.7. `ocvu_net_handle` の所有権規約(M7c で追加) + +**`ocvu_net_handle` は `ocvu_mat_handle` とまったく同じ所有権の形を再利用する。** +新しい所有権モデルは増えていない —— `native/src/ocvu_dnn_table.h` / `.cpp` は +`native/src/ocvu_mat_table.h` / `.cpp` の設計をそのまま写している(世代番号 + +索引の handle、`std::vector` の `Slot` が `std::unique_ptr` を +持つ、`std::mutex` で保護、free list で索引を再利用)。 + +**native が常に所有する。** `ocvu_dnn_net_read_onnx` が読み込んだ +`cv::dnn::Net` は、`ocvu_dnn_net_release` を呼ぶまで native 側の table が +生かし続ける。Unity 側が `cv::dnn::Net` の実体を直接見ることは無い —— 見えるのは +`uint64_t` の handle だけである。 + +**§1.5 の複数スレッド規約は `ocvu_net_handle` にもそのまま適用される。** +`net_table_get` は table の mutex を取って `cv::dnn::Net*` を解決し、 +`ocvu_mat_table.cpp` と同じくロックを離してから返す —— `setInput` / +`forward()` はロックの外で走る。table 自体は §1.5 のとおりスレッド安全だが、 +`cv::dnn::Net` 自体はそうではない。**`cv::Mat` より踏みやすい経路である** +可能性が高い —— 1 度読み込んだ `Net` を複数フレーム・複数スレッドから +使い回す使い方(1 スレッドが推論している最中に別スレッドが同じ handle を +解放する、あるいは同じ handle を 2 スレッドへ同時に渡す)は `Mat` の +一時的な読み書きより自然に起こりうる。§1.5 が明記しているとおり、 +書かれていない契約は守りようがない —— この 1 文がその契約である +(レビュー指摘 I4)。 + +**`Slot` が値ではなく `unique_ptr` を持つ理由は mat_table と同じである。** +値で持つと、table が伸びたときに再配置が起き、先に解決したポインタが +全部ぶら下がる。壊れるのは伸ばした側ではなく、無関係な handle を使っている側 +である(M3 の PR #8 で mat_table が実際に踏んだ形。上の §1.5 参照)。 +`native/tests/test_dnn_table_stability.cpp` がこれを決定的に固定している —— +容量(要素数ではない)を前後で測り、実際に再配置が起きたことまで確かめる +(`mat_table_slot_capacity()` と同じ理由。**net_table を使うのが +`test_dnn_table_stability.cpp` だけだった間は「4096 個足せば再配置される +だろう」に頼っていたが、Task 3 が `native/tests/test_dnn.cpp` から同じ +net_table を使い始めた時点でその前提が崩れる**ので、`net_table_slot_capacity()` +を足して実測に変えた)。 + +**解放は `ocvu_dnn_net_release` の 1 本だけ。** `ocvu_mat_release` と同じく、 +handle が 0、解放済み、未知の索引のいずれでも `OCVU_STATUS_INVALID_HANDLE` +を返し、落とさない(二重解放の検出)。 + +**engine / backend を選ぶ引数も定数も出さない**(設計 D7)。上流の 5.1 で +`enum EngineType` の値が総入れ替えになった(§2 冒頭の引用、docs/roadmap.md の +M7 節)ので、5.1 で意味が変わる数字を境界の外へ出さない。 + +**推論の出力は 2 次元に潰す(決定 A)。** `cv::dnn::Net::forward()` が返すのは +4 次元の blob(NCHW)だが、この ABI の `Mat` は `rows` / `cols` / `channels` / +`step` しか公開していない。`ocvu_dnn_net_forward` は最後の次元を列数として +残りをすべて行数へ畳み込む —— 分類モデルの典型的な `(1, N)` はそのまま +`1 × N` になるが、検出モデルのような `(1, C, H, W)` では **N と C の区別が +失われる。** この ABI は分類モデルの `1 × N` の出力を受け取ることを想定して +いる。選ばなかった案(別 struct で形を返す / `ocvu_tensor_handle` を新設する) +と、その理由は `docs/superpowers/plans/2026-09-05-m7c-dnn-profile.md` +「この計画で最も難しいところ」にある。 + +**`ocvu_dnn_blob_from_image` の出力は潰さない。** `cv::dnn::Net::setInput` が +要求するのは 4 次元の NCHW そのものなので、`ocvu_dnn_net_forward` へ渡すまで +形を保つ。`cv::Mat` は内部で 2 次元より多い次元を表現できるので、この +handle へ 4 次元の値を割り当てること自体は合法である。**4 次元を 2 次元へ +潰す決定は、推論の出力側だけに掛かる。** + +**~~ただし `ocvu_mat_get_info` のような 2 次元前提の関数へこの handle を +渡した結果は未定義とする。~~ この文はレビューで否定された(Important 1)。** +「未定義」ではなく**定義されているが間違っていた** —— `ocvu_mat_get_info` +は dims > 2 のとき OpenCV が返す `rows = cols = -1` をそのまま +`OCVU_STATUS_OK` で通し、`total_bytes` まで負の値になっていた。C# から +到達可能な状態だったので、いまは `ocvu_mat_get_info` 自身が +`OCVU_STATUS_INVALID_ARGUMENT` で断る(§3.14、`native/src/ocvu_mat.cpp`)。 +消さずに残してあるのは、「境界の外側は未定義でよい」という言い回しが +他のどこかで同じ誤りを隠していないか、次に読む人が確かめられるように +するためである。 + +**ファイルパスは受けない。** 理由は §1.6 と同じ(Windows の文字コード、 +Android の StreamingAssets)。 + +**ABI version は上げていない。** 新しい module と新しい handle 型 +(`ocvu_net_handle`)を足しただけなので `OCVU_ABI_VERSION` は 1 のままである +—— この判断の一般原則(「新しい関数を足す」「新しい status を末尾に足す」は +bump しない)は §2「bump しない変更」が正本として持つ。 + ## 2. C ABI の versioning と後方互換 > **上流の版を上げるときの前提**(2026-08-30 に確認)。OpenCV の @@ -308,6 +388,12 @@ native library と C# は同じ UPM パッケージで同時に配布される - 実装の内部変更で、観測できる挙動が変わらないもの - コメント・文書 +**実例(3 つ目、1 つ目の具体化)**: M7c で `ocvu_net_handle` という新しい +handle 型と `dnn` module の 4 関数を足したが、`OCVU_ABI_VERSION` は 1 の +ままである。既存の型・関数・status は 1 つも変えていない —— 「新しい関数を +足す」の一種であって、新しい所有権モデルでもない(`ocvu_mat_handle` と +同じ世代番号つき handle table を再利用している)。詳細は §1.7。 + **実例(2 つ目)**: `OCVU_STATUS_NOT_FOUND` は M5 で `OCVU_STATUS_LIST` の末尾に足した status で、bump しなかった。`ocvu_qr_decode` が「QR コードが写っていない」ことを `OCVU_STATUS_OK` + 長さ 0(=「空文字列の QR」と区別が付かない)ではなくこの新しい @@ -353,11 +439,11 @@ status 表の同期は `StatusCodeSyncTests` が見ている。**この 2 つを --- -## 3. API の allowlist(M2 で確定、M3.5・M5 で追加) +## 3. API の allowlist(M2 で確定、M3.5・M5・M7c で追加) M2 で公開する `ocvu_` 関数は次で全部とする。広さを追わないのが M2 の目的である。 -**M3.5 で 2 本、M5 で 7 本、2026-09 の API 拡張で 26 本足したので、現在の -allowlist は §3.5〜§3.13 を含めて 44 本である。** +**M3.5 で 2 本、M5 で 7 本、2026-09 の API 拡張で 26 本、M7c で 4 本足したので、 +現在の allowlist は §3.5〜§3.14 を含めて 48 本である。** **この節は「何を出すと決めたか」の正本であって、「いま何が出ているか」の一覧ではない。** 後者は `bindings/spec/*.json`(機械可読の正本)と、そこから生成される @@ -696,6 +782,108 @@ SIFT は `create(200)` で 240 個)。**`capacity` を `max_features` と同 強制するのは `StereoBM` だけで、`StereoSGBM` はどちらも検査しない(実測)—— **この ABI が自分で決めた、OpenCV より厳しい契約である**(呼ぶ側にとって単純になる)。 +### 3.14 dnn(M7c で追加。**`dnn` module と `dnn` profile を初めて足した**) + +| 関数 | 何を | +| --- | --- | +| `ocvu_dnn_net_read_onnx` | メモリ上の ONNX の byte 列から `cv::dnn::Net` を読み、`ocvu_net_handle` を返す | +| `ocvu_dnn_net_release` | `ocvu_net_handle` を解放する | +| `ocvu_dnn_blob_from_image` | `Mat` を推論の入力(4 次元の blob)にする | +| `ocvu_dnn_net_forward` | 推論を 1 回走らせ、結果を 2 次元の `Mat` に書く | + +**`ocvu_net_handle` の所有権は §1.7 が正本である。** `ocvu_mat_handle` と +まったく同じ形(native が常に所有、世代番号つき handle table)を再利用して +おり、新しい所有権モデルは増えていない。 + +**`dnn` は spec の `profile` が `standard` ではない初めての module である。** +`bindings/spec/dnn.json` は `"profile": "dnn"` を宣言しており、C ヘッダ +(`native/include/ocvu/dnn.h`)は他の module と同じ場所に出るが、C# の +P/Invoke 宣言だけが別 assembly(`CvUnity.Interop.Dnn` の `NativeMethodsDnn`) +へ出る。**§4 が合成 spec で実証していた機構を、本物の spec で初めて動かした +形である**(§4「いま実証されていること、いないこと」)。 + +**推論の出力は 2 次元に潰す(決定 A)。** `cv::dnn::Net::forward()` は 4 次元 +の blob(NCHW)を返すが、この ABI の `Mat` は 2 次元までしか表現できない。 +`ocvu_dnn_net_forward` は最後の次元を列数として残りをすべて行数へ畳み込む +—— 分類モデルの `(1, N)` はそのまま `1 × N` になるが、検出モデルのような +出力では N と C の区別が失われる。選ばなかった案(別 struct で形を返す / +`ocvu_tensor_handle` を新設する)は +`docs/superpowers/plans/2026-09-05-m7c-dnn-profile.md` +「この計画で最も難しいところ」にある。**`ocvu_dnn_blob_from_image` の出力は +潰さない** —— `cv::dnn::Net::setInput` が要求する 4 次元の NCHW をそのまま +保つ(詳細は §1.7)。 + +**engine / backend を選ぶ引数も定数も出していない**(設計 D7)。上流の 5.1 で +`enum EngineType` の値が総入れ替えになった(§2 冒頭の引用)ので、5.1 で +意味が変わる数字を境界の外へ出さない。 + +**`mean` は配列ではなく 3 個の scalar(`mean_b` / `mean_g` / `mean_r`)である +(レビューで直した。Critical 1)。** 当初は「長さを渡さない固定 3 要素の +配列」として出していたが、それは**長さの検証を一切持たない buffer**という +形で、呼ぶ側が短い配列を渡せば境界の外を読めてしまう —— 防いでいたのは +検査ではなく summary の文章だけだった。3 個の scalar に変えたことで、 +その誤りは**表現できなくなった**(§1 が借用 handle を禁じたのと同じ +「規約で禁じるのではなく表現できなくする」考え方)。NULL を心配する必要も +消えた。`width` / `height` は buffer の長さではなく `cv::dnn::blobFromImage` +がその寸法でメモリを確保する引数なので、`OCVU_DNN_MAX_BLOB_DIM`(4096)と +いう上限を新設した —— `cv::cornerSubPix` の `win_size` で踏んだのと同じ形 +である。 + +**`ocvu_dnn_blob_from_image` の検証順序は width/height → src の handle → +dst の handle で固定してあり、spec の summary と L1 +(`Dnn.BlobFromImageChecksDimensionsBeforeHandles`)の両方がそれを記録する** +(レビュー指摘 M1)。文書だけでは、順序を変える将来の変更が気づかれずに +入りうる。 + +**`ocvu_dnn_net_forward` の出力は `.clone()` している(レビューで直した。 +Important 2)。** `cv::dnn::Net::forward()` が返す `Mat` はネットワーク内部の +バッファへの浅いコピーで、決定 A の `reshape` もヘッダだけを作り直す浅い +操作である。`.clone()` を省くと `*output_mat` は net が所有するメモリを +指したままになり、同じ net で 2 回目の forward を呼ぶと 1 回目の出力が +黙って書き換わる —— この ABI の他のすべての関数が新しく確保したメモリを +返す契約と矛盾する。**この所有権の挙動は OpenCV のヘッダに明記が無く、 +実装から推測している。** + +**Task 5 が実物の ONNX モデルで確かめようとしたが、負の対照が取れなかった +(`prove-a-check-works` skill が M6 で記録した形と同じ)。** Identity 1 ノード +のモデルと、投げ捨てた検証用の 2 層 Relu モデルの両方で forward を 2 回 +呼んだが、`.clone()` を外しても外さなくても結果が変わらなかった。 +当初の説明(`cv::Mat` の参照カウントが外側の参照を守るため再割り当てが +起きる)はレビューで「`cv::Mat::create()` の早期リターン経路は参照カウントを +見ない」と指摘されほぼ確実に誤りだと分かり、いまの主説は「OpenCV 5 の +新しい dnn engine が `forward()` のたびに新しく確保したバッファを返している」 +(機構として否定されてはいないが未確認)である。**確定できない理由は、 +`./tools/opencv.ps1 restore` が復元する木がヘッダとビルド済み lib だけで、 +`cv::dnn::Net::forward()` の実装ソースを含まないため**——読んで確定させる +経路がこのリポジトリの構成上無い。**実証しているのは境界の往復(実物の +ONNX を読み込み、blob 化し、forward を呼び、結果を読める)であって、 +`.clone()` が防ぐはずのエイリアシング欠陥そのものではない**——過大に +書かないための一文(`tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs` +の `CallingForwardTwiceWithDifferentInputsDoesNotRewriteTheFirstOutput` +docstring に実測が残っている)。 + +**`ocvu_mat_get_info` が dims > 2 の Mat を拒むようになった(レビューで +直した。Important 1)。** `ocvu_dnn_blob_from_image` が作る Mat は dims == 4 +でありうるが、`ocvu_mat_info` は rows / cols / channels / step という +2 次元の語彙しか持たない。直す前は、dims > 2 のとき OpenCV が返す +`rows = cols = -1` をそのまま `OCVU_STATUS_OK` で通しており、 +`total_bytes` まで負の値になっていた —— 「読めない」ではなく「間違った値を +自信満々で返す」形で、C# から到達可能だった。いまは +`OCVU_STATUS_INVALID_ARGUMENT` を返す(`native/tests/test_mat_lifecycle.cpp` +の `GetInfoRejectsMatsWithMoreThanTwoDimensions`)。**この検査は dnn 固有の +module には置いていない** —— `ocvu_mat_get_info` は `core` module の関数で +あり、2 次元より多い Mat を作りうる関数が増えるたびに効く。 + +**COMPONENTS への追加は Task 2 が先に行った。** `native/src/ocvu_dnn_table.cpp` +の `std::unique_ptr` の破棄に `cv::dnn::Net` の定義が要るため、 +`cmake/FindOpenCvUnityDeps.cmake` の `COMPONENTS` への `dnn` の追加は +Task 2(`dd28867`)の時点で済んでいた。**それでも binary は 1 バイトも +増えなかった** —— `native/tests/test_module_linkage.cpp` の `DnnIsLinked` が +`cv::dnn::Net` を実際に構築・参照して初めてリンクの証拠になり、この 4 本の +実装(Task 3)で初めて binary が増えた。**「`COMPONENTS` に足すだけでは +増えない」を 3 度目に実測した形になる**(M3.5 の `imgcodecs`、M5 の module +追加に続く)。 + ### まだ作らないもの `Mat` の部分参照(ROI)、型変換、算術演算、**`imgcodecs` の @@ -795,15 +983,22 @@ platform(iOS / WebGL)で `"__Internal"` にならず、Player の中で最 読まない) **切り出しは M7c の判断である。** `bindings/spec/dnn.json` が入って -本物の 2 つ目の利用者ができ、レビューを一巡させられるときに決める。 +本物の 2 つ目の利用者ができ、レビューを一巡させられるときに決める +(**2026-09-08、M7c の Task 3 で入った** —— `dnn` の C ABI 4 本、§3.14)。 ### いま実証されていること、いないこと -**`bindings/spec/dnn.json` はまだ無く、`profile: "dnn"` を宣言する module は -現時点で 1 つも無い。** `CvUnity.Interop.Dnn` と `CvUnity.Tests.Shared.Dnn` は、 +> **この節はもう正確ではない。** 次の段落は `bindings/spec/dnn.json` が +> 存在しなかった時点(合成 probe だけで機構を検証していた段階)の記録で、 +> **消さずに残してある** —— 同じ「まだ無い」という前提が別の場所にも +> 残っていないかを、次に読む人が確かめられるようにするためである。 +> 現状は直後の段落を見ること。 + +**~~`bindings/spec/dnn.json` はまだ無く、`profile: "dnn"` を宣言する module は +現時点で 1 つも無い。~~** `CvUnity.Interop.Dnn` と `CvUnity.Tests.Shared.Dnn` は、 リポジトリに commit された状態では中身が空の assembly である。 -実証されているのは次の 3 つで、**どれも実物を動かして測った**: +実証されていた(当時)のは次の 3 つで、**どれも実物を動かして測った**: 1. **`defineConstraints` が効く。** `OCVU_PROFILE_DNN` を立てれば 2 つの assembly は実際にコンパイルされ、外せば消える —— Unity 自身に問うている @@ -820,9 +1015,70 @@ platform(iOS / WebGL)で `"__Internal"` にならず、Player の中で最 3. 上の (2) の常設版が速いレーンに在る(`BindingGenerator.Tests.ps1` が 合成 spec から生成して `dotnet build` に掛ける)。**Unity は要らない** -**まだ実証されていないのは「dnn で働く」ことである** —— 上の実証は +**まだ実証されていなかったのは「dnn で働く」ことである** —— 上の実証は どれも合成した probe module によるもので、本物の `dnn` の spec も -実装も無い。**「機構が働く」ことと「dnn で働く」ことは別である。** +実装も無かった。**「機構が働く」ことと「dnn で働く」ことは別である。** + +### 2026-09-08 以降: 本物の spec で何が実証され、何がまだかを更新する + +**Task 3 で実証されたこと**(`native/tests/test_dnn.cpp`・`dev.ps1 generate`・ +`tools/tests/BindingGenerator.Tests.ps1` の実測): + +- `bindings/spec/dnn.json`(本物の 4 関数、`"profile": "dnn"`)から + `dev.ps1 generate` が `native/include/ocvu/dnn.h` と + `Runtime/Interop.Dnn/NativeMethods.Dnn.g.cs` を実際に生成した。**生成物の + `LibraryName` ブロックが platform の `#if` 分岐ごと複製されていることを + 目視で確認済み**(§4 上部が予告していたとおりの形) +- `tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs` + が本物の 4 関数を 1 回ずつ呼ぶ形で生成された +- C ABI の実装(`native/src/ocvu_dnn.cpp`)が `dnn` module としてビルドされ、 + L1(`native/tests/test_dnn.cpp`、7 件)と `ModuleLinkage.DnnIsLinked` が + 実物の `opencv_unity_native.dll` に対して green + +**まだ実証されていないこと**(Task 3 の範囲外。後続タスクの担当): + +- **Unity が実物の `NativeMethodsDnn` / `AbiReachabilityChecksDnn` を実際に + コンパイルすること。** Task 3 は Unity を 1 度も起動していない —— 検証は + L1(native)と生成器の一致検査(`dotnet` ベース)だけで、上の「実証されて + いること」の 2 番目(Unity 自身に問う `ProfileGatingTests`)はまだ本物の + spec に対しては走らせていない +- **有効な ONNX を読み込んで実際に推論すること。** `native/tests/test_dnn.cpp` + は壊れた入力に対する振る舞いだけを固定しており、正常系は実物の小さな + モデルを使う L3 に委ねてある(`docs/api-reference.md` の dnn 節にも明記) +- 利用者向けの C# 公開 API(`CvUnity.Dnn.CvDnn`)はまだ無い + +**Task 4 で実証されたこと**(2026-09-09、`tools/dev.ps1 test-managed` と +`tools/dev.ps1 test-unity-editmode` の実測。上の 1 番目と 3 番目を閉じた): + +- **利用者向けの C# 公開 API ができた**(`CvUnity.Dnn.CvDnn` / `CvNet`、 + `Runtime/Dnn/CvDnn.cs`)。`CvUnity.Interop.Dnn` と同じ + `defineConstraints`(`OCVU_PROFILE_DNN`)を持つ別 assembly + `CvUnity.Dnn` に出る(`docs/api-reference.md` §2.17) +- **これが `CvUnity.Interop`/`CvUnity.Core` の外から `CvMat.Handle` を読む + 最初の消費者だった。** `Runtime/Core/` にはそれまで `AssemblyInfo.cs` が + 無く(内部でしか `.Handle` を読んでいなかったため)、`CvUnity.Dnn` を + 追加しただけでは Unity の中でコンパイルが通らない —— これは + `dev.ps1 test-managed`(shim が全部を 1 つの assembly にまとめてしまう) + では**見えない**穴で、Step 5 の「define を立てて Unity に本当にコンパイル + させる」手順で初めて表面化した。`Runtime/Core/AssemblyInfo.cs` を新設し + `[assembly: InternalsVisibleTo("CvUnity.Dnn")]` を足して塞いだ +- **Unity が実物の `NativeMethodsDnn` / `CvDnn` / `AbiReachabilityChecksDnn` を + 実際にコンパイルした**(本物の `bindings/spec/dnn.json` に対して、Unity + 6000.3.16f1 で実測。`OCVU_PROFILE_DNN` を立てて `Library/ScriptAssemblies/` + に `CvUnity.Interop.Dnn.dll` / `CvUnity.Dnn.dll` / `CvUnity.Tests.Shared.Dnn.dll` + が現れることを確認し、立てていない既定の状態では 3 つとも現れないことも + 同じ日に確認した——`ProfileGatingTests`(`tests/UnityProject/Assets/Tests/EditMode/`) + が両方向を機械的に見る。前者は手動の一回性の確認、後者は毎回の EditMode + 実行が見る) +- L3(`DnnTests.cs`)が壊れた入力・二重解放・null/空 byte 列を固定した + (185 → 189、+4) + +**Task 4 が「まだ実証されていない」と書いていた「有効な ONNX を読み込んで +実際に推論すること」は、Task 5 が実際に行った**(`tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs`、実物の `tiny.onnx` を使う 6 件)。 +**実証しているのは境界の往復(読み込み・blob 化・forward・結果読み出し) +であって、`.clone()` が防ぐはずのエイリアシング欠陥そのものではない**—— +負の対照は取れなかった(詳細は上の §1.7 と同テストの docstring)。 +過大に読まないこと。 --- diff --git a/docs/api-map.md b/docs/api-map.md index 4e1b961..a0481f8 100644 --- a/docs/api-map.md +++ b/docs/api-map.md @@ -3,7 +3,7 @@ -**公開している C ABI は 53 本**である。C# の P/Invoke 宣言は 55 本ある。 +**公開している C ABI は 57 本**である。C# の P/Invoke 宣言は 59 本ある。 **差の 2 本は C ABI を増やさない。** 既にある C の entry point へ 別の引数の形で入る C# 側の入口で、C 側に対応する宣言が無い。 @@ -36,6 +36,10 @@ | `core` | `ocvu_bitwise` | `ocvu_bitwise` | 呼ぶ | src1 と src2 のビット演算(AND / OR / XOR)、または src1 のビット反転(NOT)を dst に入れる。dst は結果に応じて丸ごと置き換わり、src1 と同じ形状・型になる。op は OCVU_BITWISE_AND / OCVU_BITWISE_OR / OCVU_BITWISE_XOR / OCVU_BITWISE_NOT のいずれかで、それ以外は OCVU_STATUS_INVALID_ARGUMENT を返す。OCVU_BITWISE_NOT のときは src2 を一切見ない —— 無効な handle を渡しても成功する(黙って無視するのではなく、そう決めてある)。他の 3 つでは src2 の handle が無効なら OCVU_STATUS_INVALID_HANDLE を返す。src1 と src2 は同じ形状・同じ型でなければならず、違えば OCVU_STATUS_INVALID_ARGUMENT を返す —— **この検査はこの ABI が自分で行う。** OpenCV に任せると、src2 が 1 要素のときにそれを scalar とみなして全画素へ黙って展開し、OCVU_STATUS_OK ともっともらしい結果を返すためである(実測)。src と dst に同じ handle を渡してもよい。失敗したときは dst を書き換えない。 | | `core` | `ocvu_lut` | `ocvu_lut` | 呼ぶ | src の各画素の値を表で引いた値に置き換えて dst に入れる。dst は結果に応じて丸ごと置き換わり、src と同じ形状・型になる。table は 8 bit の値域(0 から 255)を全部覆う 256 バイト以上でなければならず、table_length はその バイト数 である(読むのは先頭の 256 バイトだけで、それより長い分は使わない)。256 に満たなければ何も読まずに OCVU_STATUS_INVALID_ARGUMENT を返す。src は 8 bit でなければならず、そうでなければ OpenCV が例外を投げるので OCVU_STATUS_OPENCV_ERROR になる。複数 channel の src には同じ表がすべての channel に適用される。table が NULL なら OCVU_STATUS_NULL_POINTER、handle が無効なら OCVU_STATUS_INVALID_HANDLE。src と dst に同じ handle を渡してもよい。失敗したときは dst を書き換えない。 | | `core` | `ocvu_copy_make_border` | `ocvu_copy_make_border` | 呼ぶ | src の周囲に余白を足して dst に入れる。dst は結果に応じて丸ごと置き換わり、高さが src の高さ + top + bottom、幅が src の幅 + left + right で、src と同じ型になる。top / bottom / left / right はいずれも 0 以上でなければならず、負なら OCVU_STATUS_INVALID_ARGUMENT を返す。出来上がりの高さか幅が int32_t に収まらない場合も OCVU_STATUS_INVALID_ARGUMENT を返す(OpenCV に渡すと int の中で桁あふれする)。border_type は OCVU_BORDER_CONSTANT / OCVU_BORDER_REPLICATE / OCVU_BORDER_REFLECT / OCVU_BORDER_WRAP / OCVU_BORDER_REFLECT_101 のいずれかで、それ以外は拒否する。border_value は OCVU_BORDER_CONSTANT のときにだけ使う埋め値で、全 channel に同じ値が入る(channel ごとに違う値を入れる経路は出していない)。src と dst に同じ handle を渡してもよい。handle が無効なら OCVU_STATUS_INVALID_HANDLE。OpenCV が例外を投げた場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは dst を書き換えない。 | +| `dnn` | `ocvu_dnn_net_read_onnx` | `ocvu_dnn_net_read_onnx` | 呼ぶ | メモリ上の ONNX の byte 列からネットワークを読み、handle を out_handle に書く。ファイルパスは受け取らない(Windows の文字コードと Android の StreamingAssets のため)。読めなければ OCVU_STATUS_OPENCV_ERROR を返し out_handle は変更しない。data が NULL、out_handle が NULL なら OCVU_STATUS_NULL_POINTER。length が 1 未満なら OCVU_STATUS_INVALID_ARGUMENT。 | +| `dnn` | `ocvu_dnn_net_release` | `ocvu_dnn_net_release` | 呼ぶ | ocvu_dnn_net_read_onnx が返した handle を解放する。native が所有する(docs/abi-ownership-and-versioning.md §1.7)ので、使い終わったら必ずこれで解放する。net が 0、解放済み、未知の handle のいずれでも OCVU_STATUS_INVALID_HANDLE を返し、落とさない(二重解放を検出する)。 | +| `dnn` | `ocvu_dnn_blob_from_image` | `ocvu_dnn_blob_from_image` | 呼ぶ | src を推論の入力形式(4 次元の blob。N=1, C, height, width)に変換して dst に書く。scale は画素値に掛ける係数(例: 1/255.0)。mean_b / mean_g / mean_r は各チャンネルから引く値で、この順(B, G, R)で cv::Scalar に渡す固定契約である —— 配列ではなく 3 個の scalar にしてあるので、長さを取り違えて境界の外を読むことができない。検証の順序は決まっている: まず width / height の範囲(1 以上 OCVU_DNN_MAX_BLOB_DIM 以下。外れれば OCVU_STATUS_INVALID_ARGUMENT。cv::dnn::blobFromImage がその寸法でメモリを確保するため)、次に src の handle、最後に dst の handle(どちらも無効なら OCVU_STATUS_INVALID_HANDLE)。したがって範囲外の width と無効な handle を同時に渡すと OCVU_STATUS_INVALID_ARGUMENT が返る。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数はこの handle を OCVU_STATUS_INVALID_ARGUMENT で拒む)—— 2 次元へ潰すのは ocvu_dnn_net_forward の出力側だけである。OpenCV が例外を投げた場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは dst を書き換えない。 | +| `dnn` | `ocvu_dnn_net_forward` | `ocvu_dnn_net_forward` | 呼ぶ | net に input(ocvu_dnn_blob_from_image が作った blob)を渡して推論を 1 回走らせ、結果を output に書く。出力は 2 次元の Mat に潰される(rows × cols)。4 次元の blob を返すモデル(検出など)では、N と C の区別が失われる。分類モデルの 1 × N の出力を受け取ることを想定している。output は net の内部バッファから独立した新しいメモリのコピーであり、同じ net で続けて forward を呼んでも書き換わらない。handle の検証は net、input、output の順で行い、いずれかが無効なら OCVU_STATUS_INVALID_HANDLE を返す(解放済みの net も同じ status になる)。OpenCV が例外を投げた場合、または結果が空になった場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは output を書き換えない。engine / backend を選ぶ引数は無い(上流の 5.1 で enum EngineType の値が総入れ替えになったため、5.1 で意味が変わる数字を境界の外へ出さない)。 | | `features` | `ocvu_orb_detect` | `ocvu_orb_detect` | 呼ぶ | src から ORB の特徴点を検出して out_keypoints へ書き、見つかった個数を out_count に返す。呼ぶ側は必要量を事前に知り得るので 2 回呼ぶ必要は無い(上限は max_features で、capacity がそれに満たなければ何も書かずに OCVU_STATUS_BUFFER_TOO_SMALL を返し out_count に max_features を入れる)。max_features は 1 以上 OCVU_ORB_MAX_FEATURES 以下でなければならない。**OpenCV の ORB は指定した個数を上限として守らず、それを超える数の特徴点を返すことがある**(実測)ので、この関数が max_features 個で切り詰める —— 上限は OpenCV の約束ではなく、この ABI 自身の契約である。記述子も要るなら ocvu_detect_and_compute を使うこと(こちらは記述子を計算しないので、角点だけが要る用途では記述子の計算と Mat の確保が省ける)。buffer の所有権は最初から最後まで呼ぶ側にある。 | | `features` | `ocvu_detect_and_compute` | `ocvu_detect_and_compute` | 呼ぶ | src から特徴点を検出し、同時にその記述子を計算する。特徴点は out_keypoints へ書き、記述子は out_descriptors の Mat へ入れる(Mat は結果で丸ごと置き換わり、行が特徴点 1 つ、列が記述子の次元になる —— ORB は 32 列の 8 bit 符号なし整数、SIFT は 128 列の 32 bit 浮動小数である)。記述子を平らな配列で返さないのは型も幅も検出器によって違うためで、handle のまま ocvu_match_descriptors へ渡せる。detector は OCVU_FEATURE_ORB か OCVU_FEATURE_SIFT で、それ以外は OCVU_STATUS_INVALID_ARGUMENT を返す。max_features は 1 以上 OCVU_ORB_MAX_FEATURES 以下でなければならない。**max_features は OpenCV への希望であって上限ではない** —— ORB も SIFT も指定より多くの特徴点を返すことがあるので(実測)、capacity を max_features と同じ値にしてはならない。capacity は out_keypoints の**要素数**である(バイト数ではない)—— 見つかった数に満たなければ何も書かずに OCVU_STATUS_BUFFER_TOO_SMALL を返し、out_count に**実際に見つかった数**を入れる。**溢れた場合 out_descriptors は書き換えない** —— 更新されるのは out_count だけなので、返ってきた個数で確保し直してもう一度呼ぶこと。capacity が 0 で out_keypoints が NULL なら個数の問い合わせとして扱う(それでも out_descriptors には有効な handle が要る)。**1 つも見つからないのは誤りではない** —— OCVU_STATUS_OK を返して out_count に 0 を入れる。out_count が NULL なら他の何より先に OCVU_STATUS_NULL_POINTER を返し、通ったあとはどの失敗経路でも out_count に 0 を書く。src と out_descriptors に同じ handle を渡してはならない。**検出器は呼び出しのたびに作り直される** —— この ABI の粒度は 1 枚を処理することであって、検出器を保持することではない。記述子が要らないなら ocvu_orb_detect を使うこと。handle が無効なら OCVU_STATUS_INVALID_HANDLE。OpenCV が例外を投げた場合は OCVU_STATUS_OPENCV_ERROR を返す。buffer の所有権は最初から最後まで呼ぶ側にある。 | | `features` | `ocvu_match_descriptors` | `ocvu_match_descriptors` | 呼ぶ | query の各記述子に対して train の中で最も近いものを 1 つ探し、その対応を out_matches へ書いて個数を out_count に返す。query と train は ocvu_detect_and_compute が作った記述子の Mat である。norm_type は OCVU_NORM_HAMMING か OCVU_NORM_L2 で、それ以外は OCVU_STATUS_INVALID_ARGUMENT を返す —— **検出器に合わせること**(ORB の 2 値記述子には HAMMING、SIFT の 32 bit 浮動小数の記述子には L2)。**組み合わせを誤ると OpenCV が例外を投げる** —— OCVU_NORM_HAMMING を SIFT の浮動小数の記述子に当てた場合と、query と train の記述子の型が食い違う場合は、どちらも OCVU_STATUS_OPENCV_ERROR になる(実測)。cross_check は 0 以外を真として扱い、互いに最近傍である対応だけを残す(誤対応が減るが、対応の数も減る)—— **同じ画像から作った記述子どうしでも、cross_check が 0 だと query_index と train_index は一致しないことがある**(繰り返す模様では記述子が重複し、同点のとき先に現れたほうが選ばれるため)。capacity は out_matches の**要素数**である(バイト数ではない)—— 見つかった対応の数に満たなければ何も書かずに OCVU_STATUS_BUFFER_TOO_SMALL を返し、out_count に**実際の対応数**を入れる。capacity が 0 で out_matches が NULL なら個数の問い合わせとして扱う。**1 つも見つからないのは誤りではない** —— OCVU_STATUS_OK を返して out_count に 0 を入れる。out_count が NULL なら他の何より先に OCVU_STATUS_NULL_POINTER を返し、通ったあとはどの失敗経路でも out_count に 0 を書く。距離は小さいほど似ている。image_index は、この ABI が 1 対 1 の照合しか出していないので常に 0 である。handle が無効なら OCVU_STATUS_INVALID_HANDLE。OpenCV が例外を投げた場合は OCVU_STATUS_OPENCV_ERROR を返す。buffer の所有権は最初から最後まで呼ぶ側にある。 | diff --git a/docs/api-reference.md b/docs/api-reference.md index f20e3fa..d925e82 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -15,9 +15,15 @@ §2.13、core の基本演算が §2.14、特徴点マッチングとステレオが §2.15 にある。**`WebCamTexture` 連携は M4 で足したので §2.6 にある。QR コードの符号化・復号と ORB 特徴点検出、射影変換の 推定、カメラの歪み補正とチェスボードの格子点検出は M5 で足したので §1「objdetect / -features / geometry / カメラ校正」と §2.8〜§2.11 にある。**詳しい経緯は +features / geometry / カメラ校正」と §2.8〜§2.11 にある。**M7c で `dnn` を +4 本足した** —— C ABI は §1「dnn」にあるが、`"profile": "dnn"` を宣言する +初めての module なので C# の宣言だけが別 assembly(`CvUnity.Interop.Dnn`)へ +出る。**利用者向けの `CvUnity.Dnn.CvDnn` / `CvNet` も同じ M7c で足した** +(Task 4。§2.17)—— こちらも `CvUnity.Interop.Dnn` と同じ `defineConstraints` +で切ってある別 assembly(`CvUnity.Dnn`)である。**詳しい経緯は `docs/abi-ownership-and-versioning.md` §3「API の allowlist」(M3.5 の追加は §3.5、 -M5 の追加は §3.6〜§3.9)を、所有権契約そのものは同 §1 を参照。 +M5 の追加は §3.6〜§3.9)を、所有権契約そのものは同 §1・§1.7 を、profile の +仕組みは同 §4 を参照。 対応 Unity は **6000.3 以降**(`package.json` の下限が `6000.3`。**実際に検証しているのは 6000.3.16f1 の 1 版だけ**)。**対応 platform は最新の公開版のもの**(**一覧をここに写さない** —— @@ -424,6 +430,71 @@ SIFT は `create(200)` で 240 個)。**`capacity` を `max_features` と同 強制するのは `StereoBM` だけで、`StereoSGBM` はどちらも検査しない(実測)—— **この ABI が自分で決めた、OpenCV より厳しい契約である**(呼ぶ側にとって単純になる)。 +### dnn(M7c で追加、`dnn` profile) + +**この 4 本は既定 profile ではない。** spec(`bindings/spec/dnn.json`)が +`"profile": "dnn"` を宣言しているため、C ヘッダは他と同じ +`native/include/ocvu/dnn.h` に出るが、C# の宣言は `CvUnity.Interop` の +`NativeMethods` ではなく `CvUnity.Interop.Dnn` の `NativeMethodsDnn` という +**別の assembly** に出る(`docs/abi-ownership-and-versioning.md` §4)。 +**利用者側の公開 API(`CvUnity.Dnn.CvDnn` / `CvNet`)は M7c Task 4 で足した** +—— §2.17 にある。 + +| 関数 | 何をするか | +| --- | --- | +| `ocvu_dnn_net_read_onnx` | メモリ上の ONNX の byte 列からネットワークを読み、`ocvu_net_handle` を返す。ファイルパスは受け取らない | +| `ocvu_dnn_net_release` | `ocvu_net_handle` を解放する | +| `ocvu_dnn_blob_from_image` | `Mat` を推論の入力(4 次元の blob)にする | +| `ocvu_dnn_net_forward` | 推論を 1 回走らせ、結果を 2 次元の `Mat` に書く | + +**`ocvu_net_handle` は `ocvu_mat_handle` と同じ所有権の形である。** native が +常に所有し、`ocvu_dnn_net_release` で解放するまで生きる。新しい所有権モデルは +増えていない(`docs/abi-ownership-and-versioning.md` §1.7)。 + +**engine / backend を選ぶ引数も定数も出していない。** OpenCV 5.1 で +`enum EngineType` の値が総入れ替えになったため、5.1 で意味が変わる数字を +境界の外へ出さない判断である(`docs/roadmap.md` の M7 節)。 + +**推論の出力は 2 次元に潰す。** `cv::dnn::Net::forward()` が返すのは 4 次元の +blob(NCHW)だが、この ABI の `Mat` は `rows` / `cols` / `channels` / `step` +しか公開していない。`ocvu_dnn_net_forward` は最後の次元を列数として残りを +すべて行数へ畳み込む —— **分類モデルの典型的な `(1, N)` はそのまま `1 × N` に +なるが、検出モデルのような 4 次元の出力では N と C の区別が失われる。** +この ABI が受け取れるのは分類モデルの `1 × N` の出力だけである。**戻り値は +net の内部バッファから独立したコピーであり**、同じ net で続けて forward を +呼んでも書き換わらない(`.clone()` している)。handle の検証は net → +input → output の順で行い、解放済みの net も他の無効な handle と同じ +`OCVU_STATUS_INVALID_HANDLE` になる。 + +**`ocvu_dnn_blob_from_image` の出力は潰さない。** `cv::dnn::Net::setInput` が +要求するのは 4 次元の NCHW そのものなので、`ocvu_dnn_net_forward` へ渡すまで +形を保つ。この handle を `ocvu_mat_get_info` のような 2 次元前提の関数へ渡すと +`OCVU_STATUS_INVALID_ARGUMENT` で拒まれる(`ocvu_mat_get_info` は dims > 2 の +Mat を断る)。 + +**`mean_b` / `mean_g` / `mean_r` は配列ではなく 3 個の scalar である。** +各チャンネルから引く値をこの順(B, G, R)で渡す固定契約で、配列 + 長さの +形にしていない —— 短い配列を渡して境界の外を読む、という誤りをそもそも +表現できなくするためである。NULL を心配する必要も無い。**検証の順序は +width / height の範囲 → src の handle → dst の handle で固定してある** +(範囲外の width と無効な handle を同時に渡すと `OCVU_STATUS_INVALID_ARGUMENT` +になる)。`width` / `height` は `OCVU_DNN_MAX_BLOB_DIM`(4096)以下でなければ +ならない —— buffer の長さではなく、native がその寸法でメモリを確保する +引数だからである(`ocvu_orb_detect` の `max_features` などと同じ形の上限)。 + +**有効な ONNX を読み込ませて推論する経路は、L3 で実物のモデルを使って +実証している。** 壊れた入力に対する振る舞い(NULL、負または 0 の長さ、 +無効な handle、異常な blob の寸法)は L1(`native/tests/test_dnn.cpp`)と、 +公開 API の壊れた入力・寿命・所有権(`tests/Managed/CvUnity.Tests.Managed/DnnTests.cs`。 +§2.17)が固定し、正常系(読み込み → blob 化 → forward の往復)は +自作の Identity 1 ノード ONNX(`tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx`。 +生成スクリプトはその隣の `tiny.onnx.py`)を使う `DnnInferenceTests.cs` が +`tests/Managed/CvUnity.Tests.Managed/` で見ている。**ただし、これが実証する +のは境界が往復することであって、下で述べる「戻り値が独立したコピーである」 +こと(`.clone()` の効果)そのものではない** —— そちらの経緯は次段落と +`CallingForwardTwiceWithDifferentInputsDoesNotRewriteTheFirstOutput` の +docstring を見ること。 + ### この allowlist に含まれないもの `ocvu_get_abi_version` / `ocvu_get_last_error_status` / `ocvu_get_last_error_message` / @@ -441,9 +512,12 @@ C# としての契約は §2.1 の `CopyFrom(IntPtr, …)` / `CopyTo(IntPtr, … ## 2. C# 公開 API 対象アセンブリ: `CvUnity.Core`(`Runtime/Core/`、`UnityEngine` 非参照)、 -`CvUnity.UnityIntegration`(`Runtime/UnityIntegration/`、`UnityEngine` 参照)。 -`CvUnity.Interop`(`Runtime/Interop/`)は P/Invoke 宣言のみを持ち、公開型は無い -(`NativeMethods` は `internal`)。 +`CvUnity.UnityIntegration`(`Runtime/UnityIntegration/`、`UnityEngine` 参照)、 +`CvUnity.Dnn`(`Runtime/Dnn/`、`UnityEngine` 非参照。`dnn` profile の +`defineConstraints` つきで、`OCVU_PROFILE_DNN` が無いプロジェクトではコンパイル +されない)。`CvUnity.Interop`(`Runtime/Interop/`)と `CvUnity.Interop.Dnn` +(`Runtime/Interop.Dnn/`)は P/Invoke 宣言のみを持ち、公開型は無い +(`NativeMethods` / `NativeMethodsDnn` はどちらも `internal`)。 ### 2.1 `CvUnity.CvMat` @@ -887,6 +961,45 @@ CPU から読むには GPU → CPU の転送が要る。 `test-unity-graphics` レーンは `-nographics` を付けずに走り、**CI には 配線していない。** +### 2.17 `CvUnity.Dnn.CvDnn` / `CvNet`(M7c で追加、`dnn` profile) + +`CvUnity.Dnn` アセンブリ(`Runtime/Dnn/`)。**opt-in profile である** —— +`CvUnity.Interop.Dnn` と同じ `defineConstraints`(`OCVU_PROFILE_DNN`)で +切ってあり、`OCVU_PROFILE_DNN` が無いプロジェクトではコンパイルされない。 +**片方だけを切ると壊れる** —— `CvUnity.Interop.Dnn` だけを切って `CvUnity.Dnn` +を残すと、参照先を失って利用者のプロジェクトがコンパイルエラーになる +(`docs/abi-ownership-and-versioning.md` §4)。 + +`CvNet`(`sealed class`、`IDisposable`): native が所有する `ocvu_net_handle` を +包む。**寿命の契約は `CvMat`(§2.1)と同じ** —— handle は常に native 側の +ものであり、二度目以降の `Dispose()` は no-op(`ocvu_dnn_net_release` は +解放済み handle に `OCVU_STATUS_INVALID_HANDLE` を返すだけで落ちない。 +C# 側はその戻り値を見ない)。 + +| メンバ | 内容 | +| --- | --- | +| `void Dispose()` | `ocvu_dnn_net_release` を呼ぶ。二度目以降は no-op | + +`CvDnn`(`static class`): + +| メンバ | 内容 | +| --- | --- | +| `static CvNet ReadOnnx(byte[] data)` | `ocvu_dnn_net_read_onnx` を呼ぶ。`data` が `null` なら `ArgumentNullException`、空なら `ArgumentException`。読めない byte 列は `CvNativeException`(`Status == CvStatus.OpenCvError`) | +| `static void BlobFromImage(CvMat src, CvMat dst, double scale, int width, int height, double[] mean, bool swapRb, bool crop)` | `ocvu_dnn_blob_from_image` を呼ぶ。`mean` は **B, G, R の順で 3 要素**でなければならない(native は 3 個の scalar を受け取る固定契約で、配列ではないので長さを取り違える余地が無い)。長さが違えば `ArgumentException` | +| `static void Forward(CvNet net, CvMat input, CvMat output)` | `ocvu_dnn_net_forward` を呼ぶ。`output` は net の内部バッファから独立したコピーで、同じ `net` で続けて呼んでも書き換わらない | + +**正常系(実物の ONNX モデルを読んで実際に推論する経路)は L3 で実証している。** +`DnnTests.cs`(`tests/Managed/CvUnity.Tests.Managed/`)が見るのは壊れた +入力・寿命・所有権だけだが(有効な ONNX を手で組むのは現実的でないため)、 +自作の Identity 1 ノード ONNX(`TestModels/tiny.onnx`。生成スクリプトは +`TestModels/tiny.onnx.py`)を使う `DnnInferenceTests.cs` が読み込み → blob +化 → forward の往復を実物のモデルで確かめている。**実証しているのは +境界が往復することであって、`output` が独立したコピーであること +(上の行、`.clone()` の効果)を再現テストとして証明したわけではない** —— +`.clone()` を外して再現を試みたが、この規模のモデルでは検知できなかった +(`CallingForwardTwiceWithDifferentInputsDoesNotRewriteTheFirstOutput` の +docstring 参照)。 + ## 3. 対象外(この文書に書かないもの) `Mat` の部分参照(ROI)、型変換・算術演算、**`imgcodecs` のファイルパス diff --git a/docs/roadmap.md b/docs/roadmap.md index 4440186..6549e3f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2106,16 +2106,126 @@ M3.5 節を参照)、`ocvu_imencode` / `ocvu_imdecode` を出した。ここ したがって CUDA backend は完了条件に含めない。 +**この決定は解除された(gate lift、2026-09-08、リポジトリ所有者の依頼)。** +`docs/superpowers/plans/2026-09-05-m7c-dnn-profile.md` が着手の前提として掲げていた +2 つ —— (1) module 分離が済んでいること、(2) 利用者の要望か上流の安定を示す新しい +事実が出ていること —— のうち、(1) は `### M7b の判定` が 2026-09-06 に満たした。 + +(2) は、2026-09-08 にリポジトリ所有者とのやり取りで満たした。まず、所有者が +何を決めるべきかを尋ねた(原文のまま引用する): + +> 次に dnn を別 package で配るか、同じ package の optional profile にするか。M3.5 で「全部入り tarball が配る正」と決着したので、これは「中身を足す」ではなく形を変える判断になりえます。M7c の計画に検討はありますが、決定はしていません。の部分を決めて残りのタスクを進めたいです。これは何を決めたらいいですか?そのための判断として何を判断をしたらいいですか? + +これに対し、上で挙げた 5.1 での作り直しリスク(旧エンジンの削除・`enum EngineType` +の再番号・`OPENCV_FORCE_DNN_ENGINE` の意味変化という上流の実測と、それに基づく +「5.0 で作り込むと 5.1 で作り直しになる」という帰結)を明示的に提示したうえで、 +所有者は次のとおり答えた(これも原文のまま引用する): + +> 進めてください + +**この 2 つの発言は、順序があって初めて意味を持つ。** 所有者はまず何を決めるべきか +を尋ね、作り直しの費用を提示されたうえで許可した——文脈を欠いた「進めてください」 +だけを引用したのでは、リスクを見ずに出た指示なのか、承知のうえでの指示なのかが +区別できない。 + +**この経緯の記録は、この commit にしか無い。** やり取りは所有者との会話で行われ、 +紐づく issue も ticket も、外部の記録も存在しない。ここに引用した 2 つの発言と +commit のメタデータ以外に、後から確かめる手立ては無い。 + +**元の決定を支えていた根拠は、これによって否定されてはいない。** 旧エンジンの +削除、`enum EngineType` の再番号、`OPENCV_FORCE_DNN_ENGINE` の意味変化という上流の +実測はいまも成立したままで、**上流の安定を示す新しい証拠は出ていない。** 変わった +のはただ 1 点 —— **リポジトリ所有者が、その作り直しの費用を承知のうえで受け入れる +と判断したこと**である。**したがって、5.0 の DNN API に対して作るコードは、5.1 が +来たときに作り直しが要ると見込んでよい。** 後でこれに驚く読み手が出るなら、この文を +ここに見つけられるようにしてある。 + **まだ決めていないこと**(M5 / M7 で決める) - **`OCVU_ABI_VERSION` は単一の整数のまま**である(正本は `docs/abi-ownership-and-versioning.md` §2 の「決定」。ここで再オープンしない)。 module 分離がこの決定に影響しうるなら、**正本のほうに留保を書く** -- dnn を**別 package**(`…-dnn`)で配るか、同じ package の optional profile にするか。 - **全部入り tarball を配る正にしたのは M3.5 の決着**なので、dnn を足すことは - **「中身を足す」ではなく「形を変える」**ことになりうる +- ~~dnn を**別 package**(`…-dnn`)で配るか、同じ package の optional profile にするか。~~ + **2026-09-10 に、隣接するもう 1 つの問い(全部入り tarball を 1 つの artifact の + ままにするか)も決まり、2 つとも決定になった** —— native binary を分けない + (2026-09-08 決定)ことに加え、**全部入り tarball もそのまま 1 つの artifact + でよい**と決めた。詳細と理由、間違っていた場合のコストは下の「dnn の配布の形」 - GPU backend を持つ版の platform matrix(CUDA の版 × OS)。現在の platform × 1 構成が何倍になるか(**platform 数をここに書かない。正本は `tools/opencv-config.psd1` の `Toolchains`**)。**「CUDA / cuDNN は同梱しない」と決めている間(上の決定 5)、この問いに答える対象が無い** —— 決定が再評価されて同梱する方向に変われば、この行がまた意味を持つので、消さずに残してある +**dnn の配布の形**(2 つの問いのうち 1 つだけ決まった。2026-09-08) + +**決まった: native binary を profile ごとに分けない。1 つの binary を配り、gate は +C# 側が持つ。** 好みではなく構造上の理由による —— `Packages/com.ayutaz.opencv-unity-native/Runtime/Interop/NativeMethods.cs` +は iOS と WebGL で `DllImport("__Internal")` を選ぶ(`#if (UNITY_IOS || UNITY_WEBGL) && !UNITY_EDITOR`)。 +この 2 platform では plugin が Player の binary へ静的にリンクされ、**切り替える +対象のライブラリ名がそもそも存在しない。** したがって native binary を 2 本配り +`DllImport` に選ばせるという形は、6 platform のうち 2 つで成立しない。**費用も +記録しておく** —— dnn のコードは、profile を一度も有効にしない利用者にも配られる +ことになる。 + +**決定(2026-09-10): 全部入り tarball は 1 つの artifact のままにする。** 増分を +測ったところ `release.yml` の「Assemble the release assets」が実際に落ちた +(run 34319553823、PR #72: `package が上限を超えた: 124101923 > 104857600`。 +約 18 MB の超過)。**当初は下の 3 択で比較する想定だったが、原因を実測したら +どれも要らなかった。** + +ELF のセクションを直接読んで超過の内訳を調べたところ、**犯人は dnn ではなく +Android の `.so` に前から積もっていた DWARF デバッグ情報だった。** dnn を足す前の +v0.3.0 の Android `.so`(99,463,016 バイト)を同じ方法で測っても、既に +**82%(81,220,732 バイト)が `.debug_*` セクション**で、実行に要る部分は +1 割強しか無い。dnn を足した後の実物(同じ run の実測、258,995,040 バイト)は +**86%(約 224 MB)がデバッグ情報**まで悪化しており、dnn は原因ではなく +**閾値を越えさせた側**だった。Windows は `.pdb` に分離するので実行体は増えず、 +macOS の `.dylib`(29 MB)と Linux の `.so`(デバッグ情報は総量の 7% 程度)は +問題にならない。iOS / Web は Unity 側が最終リンクする**静的アーカイブ**を配る +ため話が違い、この決定では触っていない。 + +**対応: Android の `.so` だけ、リンク後に `llvm-strip --strip-unneeded` を掛ける** +(`native/CMakeLists.txt`、`CMAKE_SYSTEM_NAME STREQUAL "Android"` に限定した +POST_BUILD)。**置き場所を選んだ理由**: `tools/opencv-config.psd1` の構成ハッシュ +(`Get-OpenCvConfigHash`)は同ファイルの内容だけを見ており `cmake/toolchains/*.cmake` +は対象外なので、toolchain file に書くと変更が構成ハッシュに反映されず、**復元済みの +OpenCV artifact がそのまま使われて flag が効かない**(16 KB page size の教訓と +同じ罠)。ここは OpenCV のビルドではなく**このプラグイン自身の最終リンク**を +変えるので、`native/CMakeLists.txt` に置けば済む。**安全性の実測**: strip 前後で +`.dynsym`(P/Invoke が解決に使う動的エクスポート)の `ocvu_` シンボルは 57 個 +すべて一致し、`tools/verify-android-page-size.ps1` が見る PT_LOAD の `p_align` +も 16384 のまま変わらない。**実測(このマシン、実際に CI が生成した run +34319553823 の 6 platform artifact を材料に、`assemble-plugins.ps1` / +`pack-upm-tarball.ps1` / `measure-package-size.ps1` を実際に流して再現)**: +Android `.so` は 258,995,040 → 24,385,832 バイト、全部入り tarball は +124,102,343 → 77,528,652 バイト(上限 104,857,600 に対して約 27 MB の余裕)。 +CI 自身での確認は `.superpowers/sdd/2026-09-05-m7c-dnn-profile/task-6-report.md` +にある。 + +**当初の 3 択(比較のため記録を残す。どれも選ばなかった)**: + +1. **上限を上げる。** 倍増を捕まえるために約 1.5 倍で意図的に設定した余裕 + (`tools/pack-upm-tarball.ps1` の行)を弱めることになる +2. **別の `-dnn` tarball を配る。** 「配る正は全部入り 1 tarball」という M3.5 の + 決着を覆すことになり、利用者は 2 つの package の版を揃える必要が生じる +3. **dnn を全部入りに入れない。** `pack-upm-tarball.ps1` の上限に縛られない経路へ + 持ち出すことになる —— これは上の「決定: native bridge を module 単位に分ける」5 が + CUDA / cuDNN について名指しした**まさにその形**である。**したがって、あの決定が + 「まだ確認していない」と明記した再配布可否の確認が、dnn 自身の配布物についても + 未解決のまま戻ってくる**(`### CUDA / cuDNN 同梱の判定` の「穴を隠さず書く」 + 1 つ目の箇条) + +**間違っていた場合のコスト**: + +- **strip は無償ではない。** `.symtab` / `.strtab` を落とすので、利用者の実機で + `opencv_unity_native.so` の内部で native crash が起きても、この binary 単体 + からは関数名を復元できない(symbolicate 用の debug package をどこにも + 発行していない)。**これは `PNG_ARM_NEON=off` と同じ形の取引であって、 + 無条件の勝ちではない** —— 直すなら「配布物と対になる debug 情報の別経路 + (例えば Play Console 向けの native debug symbol table)を用意する」ことになる +- **原因が別にある将来の増加には、この対応は効かない。** デバッグ情報の比率を + 下げただけなので、dnn や他の module が実行に要るコード自体を大きく増やせば + 上限に再び当たりうる。そのときは上の 3 択が改めて選択肢に戻る +- iOS / Web の静的アーカイブは今回 measure しただけで対応していない + (54 MB / 63 MB、いずれもリンクする側の事情が変わるため別判断)。 + そちらが将来ボトルネックになれば、この決定はその 2 platform には及ばない + **差別化としての位置づけは変えない。** 競合が書き直し前のエンジンを載せている点は [競合調査](./unity-opencv-integration-research-and-plan.md) §3 / §4.6 のとおりで、 **Unity 利用者には推論エンジンの代替がある**という理由も変わらない。**変わったのは @@ -2123,7 +2233,9 @@ M3.5 節を参照)、`ocvu_imencode` / `ocvu_imdecode` を出した。ここ **完了条件** -- profile ごとの native artifact、manifest、third-party notices +- profile ごとの native artifact、manifest、third-party notices(**2026-09-08: 「artifact」は + profile ごとに分かれた binary を意味しない** —— native binary は分けないと決めた。上の + 「dnn の配布の形」参照) - RenderTexture / native texture pointer / AsyncGPUReadback を使う低コピー経路の評価 - package size、startup time、frame time、allocation の benchmark を公開 - **`dnn` を足す前に、C ABI と C# の module 分離が済んでいること**(上の 1〜2) @@ -2132,7 +2244,7 @@ M3.5 節を参照)、`ocvu_imencode` / `ocvu_imdecode` を出した。ここ ### M7a の判定(2026-09-06。**完了条件 5 件のうち 2 件を扱う**) -**M7 は当初 3 つの計画に分ける想定だった**(`docs/superpowers/plans/2026-09-05-m7-profiles-and-performance.md`。M5 で「生成の仕組みと module 追加を同時にやると切り分けられない」と判断したのと同じ理由)—— **実際に実行されたのは M7a と M7b の 2 計画で、条件 5 は計画を経ないドキュメント上の決定として閉じた**。**M7a が担当するのは完了条件 2(低コピー経路の評価)と 3(benchmark の公開)だけである** —— 条件 1(profile ごとの native artifact 等)はまだどの計画の担当にもなっておらず、条件 4(C ABI / C# の module 分離)は M7b、条件 5(CUDA / cuDNN の再配布確認)は `### CUDA / cuDNN 同梱の判定` が担当する。**この節はその 2 件だけの判定であって、M7 全体の判定ではない** —— 条件 1・4・5 がそれぞれ何本閉じているかは、この節ではなく担当する計画・節自身の判定にある(条件 4 は `### M7b の判定`、条件 5 は `### CUDA / cuDNN 同梱の判定`、条件 1 は dnn を実際に足す計画自身の判定 —— 執筆時点でまだ無い)。ここに残数を書かないのは、担当する計画が閉じるたびにその数だけがこの節に取り残されて古くなるからである。 +**M7 は当初 3 つの計画に分ける想定だった**(`docs/superpowers/plans/2026-09-05-m7-profiles-and-performance.md`。M5 で「生成の仕組みと module 追加を同時にやると切り分けられない」と判断したのと同じ理由)—— **実際に実行されたのは M7a と M7b の 2 計画で、条件 5 は計画を経ないドキュメント上の決定として閉じた**。**M7a が担当するのは完了条件 2(低コピー経路の評価)と 3(benchmark の公開)だけである** —— 条件 1(profile ごとの native artifact 等)は当時まだどの計画の担当にもなっておらず、条件 4(C ABI / C# の module 分離)は M7b、条件 5(CUDA / cuDNN の再配布確認)は `### CUDA / cuDNN 同梱の判定` が担当する。**この節はその 2 件だけの判定であって、M7 全体の判定ではない** —— 条件 1・4・5 がそれぞれ何本閉じているかは、この節ではなく担当する計画・節自身の判定にある(条件 4 は `### M7b の判定`、条件 5 は `### CUDA / cuDNN 同梱の判定`、条件 1 は `### M7c の判定`)。ここに残数を書かないのは、担当する計画が閉じるたびにその数だけがこの節に取り残されて古くなるからである。 実装は `.superpowers/sdd/2026-09-05-m7a-low-copy-and-benchmarks/`(Task 1〜6)。実測はすべてこのマシン(Windows 10.0.22631、X64、Unity 6000.3.16f1、2026-09-05〜09-06)。詳細な数字と読み方は [性能](./performance.md) が正本で、ここには写さない。 @@ -2146,7 +2258,7 @@ M3.5 節を参照)、`ocvu_imencode` / `ocvu_imdecode` を出した。ここ - **M7a は roadmap の差別化の穴 #9(「低コピー連携」を測っていない)を「部分的に解消」にした。** 「解消済み」としなかった理由は、上の 2 経路のうち `RenderTexture` / `AsyncGPUReadback` が実機で動く実行形態(IL2CPP Player)で 1 度も検証されておらず、`test-unity-graphics` が CI に配線されていないため——**満たしたことと実証されたことは同じではない**(`milestone-complete` skill)。 - **`test-unity-player` はこのマシンで、Player の後始末段階(`Stop-UnityTestPlayers` 内の `Get-CimInstance` 呼び出し)がハングする既知の欠陥を持つ。** テスト自体は完走し結果 XML も書かれるが、レーン全体が無音で固まる(`CLAUDE.md` が書く「Unity のレーンではクラッシュもハングも赤いテストにならない」という形そのもの)。M7a の変更が原因ではない(`git diff` でこの箇所に差分は無い)ので、この作業では直していない——本番の測定は、ハングしたプロセスを終了させたうえで `tools/assert-unity-results.ps1` を結果 XML に直接掛けて確認した(35 passed / exit 0)。 - **`BenchmarkRunner` の `Report` ヘルパーが `BenchmarkRunner.cs` と `GraphicsBenchmarkRunner.cs` に複製されている。** このリポジトリは「本体はここにしか無い」を繰り返し記録しており、片方だけ直る壊れ方をする。M7a では直していない。 -- **この計画(M7a)が触れているのは条件 2・3 だけである。** dnn を opt-in profile として足す前提(C ABI / C# の module 分離、条件 4)にも、CUDA / cuDNN の再配布確認(条件 5)にも、条件 1(profile ごとの native artifact 等)にも触れていない。**それぞれの現在の状態は、この節ではなく担当する計画自身の判定にある**(条件 4 は `### M7b の判定`、条件 5 は `### CUDA / cuDNN 同梱の判定`、条件 1 は dnn を実際に足す計画自身の判定 —— 執筆時点でまだ無い)。 +- **この計画(M7a)が触れているのは条件 2・3 だけである。** dnn を opt-in profile として足す前提(C ABI / C# の module 分離、条件 4)にも、CUDA / cuDNN の再配布確認(条件 5)にも、条件 1(profile ごとの native artifact 等)にも触れていない。**それぞれの現在の状態は、この節ではなく担当する計画自身の判定にある**(条件 4 は `### M7b の判定`、条件 5 は `### CUDA / cuDNN 同梱の判定`、条件 1 は `### M7c の判定`)。 ### M7b の判定(2026-09-06。**完了条件 5 件のうち 1 件を扱う**) @@ -2163,11 +2275,11 @@ M3.5 節を参照)、`ocvu_imencode` / `ocvu_imdecode` を出した。ここ - **非既定 profile の生成物は、2026-09-06 までコンパイルできなかった。** `[DllImport(LibraryName, ...)]` の `LibraryName` は手書きの `Runtime/Interop/NativeMethods.cs` にある `internal const` で、既定 profile はそれと同じ型の `partial` だから見えていた。**非既定 profile は別 assembly・別型なので見えず、CS0103 になる。** 最終レビューが合成 spec から生成してコンパイルし、実測して見つけた。**この branch の検査はどれも捕まえなかった** —— `ProfileTests` は出力の**文字列**、`BindingGenerator.Tests.ps1` は `--list-outputs` が返す**パス**、Task 4 の Unity 側の正の対照は**手書きの probe** を見ており、**生成物をコンパイルする経路が 1 本も無かった。** 直した形(生成器が `LibraryName` を `#if` 分岐ごと複製し、写し 2 つを `ProfileTests` が読み比べる)と、切り出さなかった理由は [所有権と versioning](./abi-ownership-and-versioning.md) §4 にある。 - **`ProfileGatingTests` が自動で見られるのは「切れている」方向だけである。** define を立てれば現れることは、define を変えて Unity をもう一度走らせないと確かめられない —— 1 回の EditMode 実行では原理的に届かないので、**正の方向は人が手で確かめる**(手順と最後の実測日は同テストの docstring にある)。綴り間違いだけは機械が塞いである(asmdef が実際に綴っている値と、テストが使う定数を突き合わせる)。 - **native の module 選択と C# の profile は別の軸で、互いを自動では決めない。** ある module を `OCVU_MODULES` に足しても、対応する spec の `profile` を書き換えない限り、その宣言は `standard` の assembly に出続ける。 -- **この節が閉じたのは条件 4 だけである。** 条件 2・3 の状態は `### M7a の判定`、条件 5 の状態は `### CUDA / cuDNN 同梱の判定`、条件 1 の状態は dnn を実際に足す計画自身の判定にある —— 執筆時点でまだ無い。**ここに残数を書かないのは、`### M7a の判定` が「残る 3 件」と書いて M7b がそれを 1 件消した瞬間に古くなったのと同じ壊れ方を、この節自身が再生産しないようにするためである。** +- **この節が閉じたのは条件 4 だけである。** 条件 2・3 の状態は `### M7a の判定`、条件 5 の状態は `### CUDA / cuDNN 同梱の判定`、条件 1 の状態は `### M7c の判定` にある。**ここに残数を書かないのは、`### M7a の判定` が「残る 3 件」と書いて M7b がそれを 1 件消した瞬間に古くなったのと同じ壊れ方を、この節自身が再生産しないようにするためである。** ### CUDA / cuDNN 同梱の判定(2026-09-06。**完了条件 5 件のうち 1 件を扱う**) -**この節が担当するのは完了条件 5(CUDA / cuDNN の再配布確認)だけである。** 条件 2・3 の状態は `### M7a の判定`、条件 4 の状態は `### M7b の判定` にある。**条件 1(profile ごとの native artifact、manifest、third-party notices)はこの節が触れていない** —— dnn を実際に足す計画(roadmap の判断待ち)が担当する。実装は伴わない —— 上の「決定: native bridge を module 単位に分ける」5 に決定を書き加えた、文書のみの変更である。 +**この節が担当するのは完了条件 5(CUDA / cuDNN の再配布確認)だけである。** 条件 2・3 の状態は `### M7a の判定`、条件 4 の状態は `### M7b の判定`、条件 1 の状態は `### M7c の判定` にある。**条件 1(profile ごとの native artifact、manifest、third-party notices)はこの節が触れていない。** 実装は伴わない —— 上の「決定: native bridge を module 単位に分ける」5 に決定を書き加えた、文書のみの変更である。 | # | 完了条件 | 判定 | | --- | --- | --- | @@ -2177,9 +2289,32 @@ M3.5 節を参照)、`ocvu_imencode` / `ocvu_imdecode` を出した。ここ - **この決定が縛るのは「同梱」だけである。** 利用者が自分の環境へ CUDA / cuDNN を別途導入してこのプラグインと組み合わせて使うこと、あるいは native artifact をいまの全部入り tarball とは別の経路(`tools/pack-upm-tarball.ps1` の上限に縛られない配布形態)で配ることには、この決定は及ばない。**そちらを選ぶ日が来たら、読んでいない再配布条件の確認がそのまま未解決で戻ってくる** —— 大きさが理由の決定は、大きさの制約が外れた瞬間に理由を失う。再評価の条件は決定本文(上の「決定: native bridge を module 単位に分ける」5)に書いてある。 - **「CUDA / cuDNN のライセンスは確認済み」と読んではいけない。** 確認したのは大きさだけで、ライセンス条項は今回も「確かめていない」側のままである。 -- **この節が閉じたのは条件 5 だけである。** 条件 1 の状態はそれを担当する計画自身の判定にある。 +- **この節が閉じたのは条件 5 だけである。** 条件 1 の状態は `### M7c の判定` にある。 - **判定節どうしの相互参照は散文であり、何も検査していない。** `ci-lint.yml` の documentation link check が見るのは通常の markdown リンク記法(角括弧の直後に丸括弧でリンク先を書く形)だけで、見出し名を backtick で引用しただけの参照(`M7c 自身の判定` のような文言)は対象外——実際、この節を新設した際に `### M7a の判定` と `### M7b の判定` の末尾がそれぞれ「条件 1・5 は M7c 自身の判定」と書いたまま古くなっているのを見つけて手で直した(本節を書いたコミットの直後)。**この警告を書いたその branch の中で、さらに 1 件(`### M7a の判定` 冒頭段落、末尾 2 箇所とは別の 3 箇所目)を見逃した** —— 人が 2 人がかりで洗ってなお取りこぼした実例であり、「原理的に壊れやすい」という一般論ではなく、この branch で実際に起きたことである。**次に判定節を足す・条件を閉じる人は、既存の判定節にある名指しの参照を自分で洗って直す必要があり、それを見落としても機械は気づかない。** +### M7c の判定(2026-09-10。**完了条件 5 件のうち 1 件を扱う**) + +**この節が担当するのは完了条件 1(profile ごとの native artifact、manifest、third-party notices)だけである。** 条件 2・3 の状態は `### M7a の判定`、条件 4 の状態は `### M7b の判定`、条件 5 の状態は `### CUDA / cuDNN 同梱の判定` にあり、いずれもこの節では触れない(条件 5 は既に閉じており、ここで二重に判定しない)。実装は `.superpowers/sdd/2026-09-05-m7c-dnn-profile/`(Task 1〜6)。実測はすべてこのマシン(Windows 10.0.22631、X64、2026-09-08〜09-10)と、参照する CI run による。 + +| # | 完了条件 | 判定 | +| --- | --- | --- | +| 1 | profile ごとの native artifact、manifest、third-party notices | **満たした。ただし artifact の「profile ごと」は native binary の分割を意味しない**(上の「dnn の配布の形」で 2026-09-08 に決定済み——native は 1 本、gate は C# 側)。native artifact: `dnn` は `tools/opencv-config.psd1` の `Modules` に足した 7 つ目の module で(構成ハッシュが `4785d98e9aad` 系から変わり 6 platform 分を作り直した)、`cmake/FindOpenCvUnityDeps.cmake` の `COMPONENTS` にも足したのでリンク済み module は 8 → 9 になった。manifest: 実際に CI が生成した `build-manifest.json`(run 34319553823、android-arm64)を読んで確認済み——`requestedModules` / `builtModules` の両方に `dnn` が現れる。third-party notices: `protobuf`(`WITH_PROTOBUF=ON` / `BUILD_PROTOBUF=ON`)を allowlist と notices の両方に足した(`ea5697e` / `81dbba9`)。**配布物としては、この Task 6 まで存在しなかった**——全部入り tarball が `Assemble the release assets`(必須チェック)で `package が上限を超えた: 124101923 > 104857600` として落ちており、**profile ごとの artifact を満たす前提である「配れること」自体が欠けていた。** 原因は dnn ではなく Android `.so` に前から積もっていた DWARF デバッグ情報(詳細は上の「dnn の配布の形」)で、`native/CMakeLists.txt` に Android 限定の `llvm-strip --strip-unneeded` を足して解消した——実測は同じ節にある。 | + +**満たしたが、実証はしていない。** + +- **実機で `dnn` を動かしたことは 1 度も無い。** M4 が残した「Android / iOS は CI がビルドするが誰も動かしたことがない」という穴は、`dnn` についても同じ形のまま変わっていない。 +- **Web での `dnn` の動作は CI の browser E2E に任せており、手元では確かめていない。** +- **推論の速さを測っていない。** `dev.ps1 benchmark` の対象に `dnn` の項目は無く、`OCVU_BENCH:` 行を `dnn` の forward 呼び出しから出したことも無い。 + +**穴を隠さず書く。** + +- **`ocvu_dnn_net_forward` の `.clone()` は必須だと実証されていない。** `native/src/ocvu_dnn.cpp` の当該行は「返す `cv::Mat` が net 内部バッファを指したままだと 2 回目の forward が 1 回目の出力を書き換える」という前提で書かれているが、Task 5 が実物の ONNX(Identity 1 ノード、2 層 Relu)で 2 回 forward を呼ぶ負の対照を試みたところ、`.clone()` を外しても検知できなかった。当初の説明(`cv::Mat` の参照カウントが外側の参照を守るため再割り当てが起きる)はレビューで「`cv::Mat::create()` の早期リターン経路は参照カウントを見ない」と指摘されほぼ確実に誤りと分かり、いまの主説は「OpenCV 5 の新しい dnn engine が `forward()` のたびに新しく確保したバッファを返している」(機構として否定はされていないが未確認)に変わっている。**確定できない理由は、`./tools/opencv.ps1 restore` が復元する木がヘッダと lib だけで、`cv::dnn::Net::forward()` の実装ソースを含まないため**——読んで確定させる経路がこのリポジトリの構成上無い。`.clone()` は「念のための安全装置」ではなく「返した handle が独立したメモリを持つという契約を成立させている唯一のもの」として残してあるが、**その必要性を示す再現テストはまだ無い。** +- **MLAS / ONNX Runtime のソースは依存 allowlist から見えない。** `tools/verify-opencv-artifact.ps1` の allowlist は artifact の中の**独立したライブラリ**を検査する作りで、`protobuf` はそれで捕まえられたが、MLAS / ONNX Runtime は `libopencv_dnn.a` の一部として静的に取り込まれており、独立したライブラリとして現れないので**allowlist には何も見えていない**。ライセンスディレクトリにも該当するものが無い。 +- **`WITH_CAROTENE` と `WITH_KLEIDICV` は既定 ON のまま、監査していない。** どちらも `OCV_OPTION` の既定が ON で、`WITH_IPP` / `WITH_ITT` のようにこのプロジェクトが明示的に OFF にしている他の optional 依存とは扱いが違う。macOS の実際のリンクには `libtegra_hal.a` / `libkleidicv_hal.a` / `libkleidicv_thread.a` / `libkleidicv.a` が含まれることを実測済みだが、ライセンス・再配布条件は確認していない。 +- **third-party のライセンス集合は platform ごとに実際に違うが、notices はそれを反映していない。** `clapack-lapack_LICENSE` は Linux / Windows / Android / Web には存在するが macOS / iOS には無い(`THIRD_PARTY_NOTICES.md` は 1 通の文書で全 platform を代表している)。 +- **`PNG_ARM_NEON=off` は取引であって、無条件の勝ちではない。** `cb250c2` で android-arm64 / ios-arm64 / macos-arm64 の 3 platform に限定して立てた。原因は上流 OpenCV 5.0.0 の vendoring 欠陥——`3rdparty/mlas/lib/compute.cpp` の `MlasGQASupported` が `MlasHGemmSupported()` を無条件に呼ぶが実体が vendor されておらず、macOS arm64 のリンクが `MlasHGemmSupported` 未定義で落ちる。その ASM 有効化の真因を辿ると `3rdparty/libpng/CMakeLists.txt` の ARM NEON 向け `enable_language(ASM)` に行き着き、`PNG_ARM_NEON=off` はこれを止める代わりに `arm/filter_neon.S` だけでなく同じ分岐にある `arm_init.c` / `filter_neon_intrinsics.c` / `palette_neon_intrinsics.c` も道連れにする——**PNG は arm64 3 platform で ARM 加速を丸ごと失う。** 上流には intrinsics だけ残す形(QNX 向け)があるが、このプロジェクトからは触れない。5.1 で上流が直せば見直す価値がある。 +- **dnn の到達性テストは、レビューまでどこからも呼ばれていなかった。** 生成物 `AbiReachabilityChecksDnn.CallEveryEntryPoint()`(`tests/UnityProject/Assets/Tests/Shared.Dnn/`)は spec の dnn 4 関数を 1 回ずつ呼ぶために存在するが、標準 profile の同じ仕組み(`AbiSurfaceTests.cs` / `AbiSurfacePlayerTests.cs` / `WebSmokeRunner.cs` の 3 箇所から呼ばれる)と違い、呼び出し元が 1 つも無かった。**呼ばれない宣言そのものが IL2CPP の stripping の対象になる**ので、この検査は「無い」以上に悪い状態だった——M4 が手書きの 19 本のうち 7 本で実際に踏んだのと同じ形の穴が、dnn の 4 本に開いたまま気づかれずにいた。レビューで指摘され、`tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs`(`#if OCVU_PROFILE_DNN` で自分を守る、`CvUnity.Tests.PlayMode` の asmdef に `CvUnity.Tests.Shared.Dnn` への参照を足した)を追加して閉じた。**手で 1 回、`OCVU_PROFILE_DNN` を立てて `test-unity-player` を実際に走らせ、stripping が dnn の 4 宣言を消していないことを確認した**(`ProfileGatingTests` が確立した「正の方向は人が手で確かめる」規約と同じ形。実測の日付と結果はこの節の下、または `tools/dev.ps1` 実行ログを参照)。**CI はどのレーンも `OCVU_PROFILE_DNN` を立てないので、この確認は一度きりであり、次に dnn の ABI が変わったときに自動では再検証されない。** + --- ## 担当が無かった制約(2026-08-29 に全件割り当て、うち 1 件は M3.5 で解消) diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index da0a502..c9beece 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -94,6 +94,46 @@ set_target_properties(opencv_unity_native PROPERTIES OUTPUT_NAME opencv_unity_native ) +# **Android だけ、リンク後に DWARF デバッグ情報を strip する。** +# +# dnn(実質は MLAS / onnxruntime 由来の重いテンプレート)を足したことで +# 全部入り tarball が上限(100 MB)を 18 MB 超えた。ELF のセクションを +# 直接読んで原因を実測したところ、Android の .so は **86% が +# .debug_* セクション**で、実行に要る部分(.text / .rodata / .dynsym 等)は +# 1 割に満たない。**これは dnn 固有の問題ではない** —— dnn を足す前の +# v0.3.0 の .so を同じ方法で測っても、既に 82% がデバッグ情報だった。 +# dnn は閾値を越えさせただけで、原因は前から在った。 +# +# Windows は `.pdb` にデバッグ情報を分離するので実行体は増えず、macOS の +# .dylib は約 14 MB で問題にならない。iOS / Web は Unity が最終リンクする +# **静的アーカイブ**を配るので、話が違う(strip すると利用側のリンクが +# 変わりうる)——この決定はここでは行わない。 +# +# **`--strip-unneeded` を選ぶ。** `.debug_*` に加えて `.symtab` / `.strtab` +# (backtrace 用のローカル記号)も落とすが、実行時に解決へ使う `.dynsym` / +# `.dynstr` は対象外(objcopy/strip の仕様)。実測でも確認済み: strip 前後で +# `ocvu_` の動的エクスポート(`.dynsym`)は 1 つも減らない。 +# +# **CMAKE_STRIP を使う。** NDK の toolchain file +# (`${ANDROID_NDK}/build/cmake/android.toolchain.cmake`)が +# `llvm-strip` を指すよう設定済みなので、道具を自分で探さない。 +# **見つからなければ黙って skip しない** —— 「strip できたはず」が +# 検証されないまま配られる経路を作らないため、無ければ止める。 +if(CMAKE_SYSTEM_NAME STREQUAL "Android") + if(NOT CMAKE_STRIP) + message(FATAL_ERROR + "CMAKE_STRIP が未設定です。NDK の toolchain file が正しく " + "include されているか確認すること(Android の .so から " + "デバッグ情報を strip できません)。") + endif() + add_custom_command( + TARGET opencv_unity_native POST_BUILD + COMMAND ${CMAKE_STRIP} --strip-unneeded "$" + COMMENT "Android: stripping DWARF debug info from opencv_unity_native.so" + VERBATIM + ) +endif() + add_library(ocvu_static STATIC ${OCVU_SOURCES}) target_include_directories(ocvu_static PUBLIC $ diff --git a/native/include/ocvu/dnn.h b/native/include/ocvu/dnn.h new file mode 100644 index 0000000..b7dbe41 --- /dev/null +++ b/native/include/ocvu/dnn.h @@ -0,0 +1,31 @@ +/* + * このファイルは生成物である。手で編集しないこと。 + * 正本: bindings/spec/dnn.json + * 生成: ./tools/dev.ps1 generate + */ +#ifndef OCVU_DNN_H +#define OCVU_DNN_H + +#include "opencv_unity_native.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* メモリ上の ONNX の byte 列からネットワークを読み、handle を out_handle に書く。ファイルパスは受け取らない(Windows の文字コードと Android の StreamingAssets のため)。読めなければ OCVU_STATUS_OPENCV_ERROR を返し out_handle は変更しない。data が NULL、out_handle が NULL なら OCVU_STATUS_NULL_POINTER。length が 1 未満なら OCVU_STATUS_INVALID_ARGUMENT。 */ +OCVU_API ocvu_status ocvu_dnn_net_read_onnx(const uint8_t* data, int32_t length, ocvu_net_handle* out_handle); + +/* ocvu_dnn_net_read_onnx が返した handle を解放する。native が所有する(docs/abi-ownership-and-versioning.md §1.7)ので、使い終わったら必ずこれで解放する。net が 0、解放済み、未知の handle のいずれでも OCVU_STATUS_INVALID_HANDLE を返し、落とさない(二重解放を検出する)。 */ +OCVU_API ocvu_status ocvu_dnn_net_release(ocvu_net_handle net); + +/* src を推論の入力形式(4 次元の blob。N=1, C, height, width)に変換して dst に書く。scale は画素値に掛ける係数(例: 1/255.0)。mean_b / mean_g / mean_r は各チャンネルから引く値で、この順(B, G, R)で cv::Scalar に渡す固定契約である —— 配列ではなく 3 個の scalar にしてあるので、長さを取り違えて境界の外を読むことができない。検証の順序は決まっている: まず width / height の範囲(1 以上 OCVU_DNN_MAX_BLOB_DIM 以下。外れれば OCVU_STATUS_INVALID_ARGUMENT。cv::dnn::blobFromImage がその寸法でメモリを確保するため)、次に src の handle、最後に dst の handle(どちらも無効なら OCVU_STATUS_INVALID_HANDLE)。したがって範囲外の width と無効な handle を同時に渡すと OCVU_STATUS_INVALID_ARGUMENT が返る。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数はこの handle を OCVU_STATUS_INVALID_ARGUMENT で拒む)—— 2 次元へ潰すのは ocvu_dnn_net_forward の出力側だけである。OpenCV が例外を投げた場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは dst を書き換えない。 */ +OCVU_API ocvu_status ocvu_dnn_blob_from_image(ocvu_mat_handle src, ocvu_mat_handle dst, double scale, int32_t width, int32_t height, double mean_b, double mean_g, double mean_r, int32_t swap_rb, int32_t crop); + +/* net に input(ocvu_dnn_blob_from_image が作った blob)を渡して推論を 1 回走らせ、結果を output に書く。出力は 2 次元の Mat に潰される(rows × cols)。4 次元の blob を返すモデル(検出など)では、N と C の区別が失われる。分類モデルの 1 × N の出力を受け取ることを想定している。output は net の内部バッファから独立した新しいメモリのコピーであり、同じ net で続けて forward を呼んでも書き換わらない。handle の検証は net、input、output の順で行い、いずれかが無効なら OCVU_STATUS_INVALID_HANDLE を返す(解放済みの net も同じ status になる)。OpenCV が例外を投げた場合、または結果が空になった場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは output を書き換えない。engine / backend を選ぶ引数は無い(上流の 5.1 で enum EngineType の値が総入れ替えになったため、5.1 で意味が変わる数字を境界の外へ出さない)。 */ +OCVU_API ocvu_status ocvu_dnn_net_forward(ocvu_net_handle net, ocvu_mat_handle input, ocvu_mat_handle output); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* OCVU_DNN_H */ diff --git a/native/include/opencv_unity_native.h b/native/include/opencv_unity_native.h index f138846..2f50a9e 100644 --- a/native/include/opencv_unity_native.h +++ b/native/include/opencv_unity_native.h @@ -81,6 +81,14 @@ enum { OCVU_STATUS_LIST(OCVU_STATUS_ENUMERATOR_) }; typedef uint64_t ocvu_mat_handle; #define OCVU_MAT_HANDLE_NONE ((ocvu_mat_handle)0) +/** + * 読み込んだニューラルネットワークの handle。 + * + * **native が所有する。** ocvu_dnn_net_release で解放するまで生きる。 + * ocvu_mat_handle と同じ規約で、0 は常に無効である。 + */ +typedef uint64_t ocvu_net_handle; + /* * ABI に出す Mat の型。 * @@ -389,6 +397,15 @@ typedef struct ocvu_dmatch { #define OCVU_IMREAD_GRAYSCALE 0 #define OCVU_IMREAD_COLOR 1 +/* ocvu_dnn_blob_from_image の width / height の上限。 + * OCVU_ARUCO_MAX_MARKER_PIXELS と同じ理由 —— この値は buffer の長さではなく、 + * cv::dnn::blobFromImage が実際にその寸法でメモリを確保する引数である + * (cv::cornerSubPix の win_size で踏んだのと同じ形。add-abi-function skill の + * 「buffer ではないのに上限が要る引数」)。縛らないと呼ぶ側が渡した + * int32_t からそのまま巨大な確保が起きる。実用上の推論入力(せいぜい + * 数百〜1000 台)はこれを大きく下回る。 */ +#define OCVU_DNN_MAX_BLOB_DIM 4096 + /* * module ごとの宣言は生成物である(bindings/spec/*.json を正本として * ./tools/dev.ps1 generate が書き出す)。**ここに手で足さないこと。** @@ -407,5 +424,6 @@ typedef struct ocvu_dmatch { #include "ocvu/geometry.h" #include "ocvu/calib.h" #include "ocvu/stereo.h" +#include "ocvu/dnn.h" #endif /* OPENCV_UNITY_NATIVE_H */ diff --git a/native/modules.cmake b/native/modules.cmake index 2e977ae..4d184bf 100644 --- a/native/modules.cmake +++ b/native/modules.cmake @@ -37,10 +37,15 @@ set(OCVU_MODULE_geometry src/ocvu_geometry.cpp src/ocvu_pose.cpp) set(OCVU_MODULE_calib src/ocvu_calibration.cpp) set(OCVU_MODULE_stereo src/ocvu_stereo.cpp) +# **Task 3 が ocvu_dnn.cpp(C ABI 関数本体)を足した。** +# Task 2 の時点ではこの 1 本(handle 表)だけだった —— src/ocvu_dnn.cpp が +# まだ存在せず、列挙すると configure が落ちるためである。 +set(OCVU_MODULE_dnn src/ocvu_dnn_table.cpp src/ocvu_dnn.cpp) + # **infra と core は外せない。** last-error も status も Mat も、 # 他の全 module が使う。外せる単位から明示的に除く。 set(OCVU_REQUIRED_MODULES infra core) # 既定で全部入れる。profile はこの一覧から引く形で表す。 set(OCVU_ALL_MODULES - infra core imgproc imgcodecs objdetect features geometry calib stereo) + infra core imgproc imgcodecs objdetect features geometry calib stereo dnn) diff --git a/native/src/ocvu_dnn.cpp b/native/src/ocvu_dnn.cpp new file mode 100644 index 0000000..e4bfa4d --- /dev/null +++ b/native/src/ocvu_dnn.cpp @@ -0,0 +1,198 @@ +// dnn module。**bindings/spec/dnn.json の "profile": "dnn" を初めて +// 本物の spec で実運用する場所である**((b) が作った機構自体は合成 spec で +// 実証済みだった。docs/abi-ownership-and-versioning.md §4)。 +// +// **engine / backend を選ぶ引数も定数も出さない**(設計 D7)。上流の 5.1 で +// `enum EngineType` の値が総入れ替えになったので(docs/roadmap.md の M7 節)、 +// 5.1 で意味が変わる数字を境界の外へ出さない。 +// +// **`cv::dnn::Net::forward()` が返すのは 4 次元の blob(NCHW)だが、この ABI の +// Mat は 2 次元までしか表現できない。** 4 次元を 2 次元へ潰す(決定 A。 +// docs/superpowers/plans/2026-09-05-m7c-dnn-profile.md「この計画で最も +// 難しいところ」)。分類モデルの 1×N の出力を想定していて、検出モデルの +// ような出力では N と C の区別が失われる —— spec の summary と +// docs/api-reference.md の両方に明記してある。 +// +// **`ocvu_dnn_blob_from_image` は潰さない。** cv::dnn::Net::setInput が +// 要求するのは 4 次元の NCHW そのものなので、ocvu_dnn_net_forward に渡す +// までは形を保つ。cv::Mat は内部で 2 次元より多い次元を表現できるので、 +// この handle へ割り当てること自体は合法である —— ただし +// ocvu_mat_get_info のような 2 次元前提の関数へこの handle を渡した +// 結果は未定義とする。 + +#include + +#include +#include + +#include +#include + +#include "ocvu_dnn_table.h" +#include "ocvu_error.h" +#include "ocvu_mat_table.h" + +extern "C" ocvu_status ocvu_dnn_net_read_onnx(const uint8_t* data, int32_t length, ocvu_net_handle* out_handle) { + OCVU_TRY_BEGIN + if (data == nullptr || out_handle == nullptr) { + return ::ocvu::set_last_error(OCVU_STATUS_NULL_POINTER, + "ocvu_dnn_net_read_onnx: data or out_handle is NULL"); + } + if (length < 1) { + return ::ocvu::set_last_error(OCVU_STATUS_INVALID_ARGUMENT, + "ocvu_dnn_net_read_onnx: length must be at least 1"); + } + + // **OCVU_TRY_END は cv::Exception を UNKNOWN_ERROR に変換する。** + // OPENCV_ERROR を返したいので、ここで自分で捕まえる + // (ocvu_stereo.cpp / ocvu_calibration.cpp と同じ作法)。 + try { + // **byte 列を複製しない。** cv::dnn::readNetFromONNX には + // (const char*, size_t) を直接受ける overload があるので、 + // std::vector へコピーしてから渡す理由が無い。100 MB 級のモデルでは + // このコピーだけで一時的にピークメモリが倍になる —— モバイルで + // 効いてくる(レビュー指摘、M3)。data は呼び出しの内側でのみ + // 読む借用で、readNetFromONNX はこの呼び出しが戻る前に内容を + // 消費し終える。 + auto net = std::make_unique( + cv::dnn::readNetFromONNX(reinterpret_cast(data), static_cast(length))); + if (net->empty()) { + return ::ocvu::set_last_error(OCVU_STATUS_OPENCV_ERROR, + "ocvu_dnn_net_read_onnx: ONNX が空のネットワークになった"); + } + // **成功してから初めて out_handle に書く。** + *out_handle = ::ocvu::net_table_add(std::move(net)); + return OCVU_STATUS_OK; + } catch (const cv::Exception& e) { + return ::ocvu::set_last_error(OCVU_STATUS_OPENCV_ERROR, e.what()); + } + OCVU_TRY_END +} + +extern "C" ocvu_status ocvu_dnn_net_release(ocvu_net_handle net) { + OCVU_TRY_BEGIN + if (!::ocvu::net_table_remove(net)) { + return ::ocvu::set_last_error(OCVU_STATUS_INVALID_HANDLE, + "ocvu_dnn_net_release: handle is unknown or already released"); + } + return OCVU_STATUS_OK; + OCVU_TRY_END +} + +extern "C" ocvu_status ocvu_dnn_blob_from_image(ocvu_mat_handle src, ocvu_mat_handle dst, double scale, int32_t width, int32_t height, double mean_b, double mean_g, double mean_r, int32_t swap_rb, int32_t crop) { + OCVU_TRY_BEGIN + // **mean は配列ではなく 3 個の scalar である(レビュー指摘、Critical 1)。** + // 配列 + 長さの形だと、呼ぶ側が短い配列を渡した場合に境界の外を読む + // ことが「表現できて」しまい、それを防ぐのは検査でしかない。3 個の + // scalar にすれば、その誤りは型として存在しない + // (docs/abi-ownership-and-versioning.md §1 が借用 handle を禁じたのと + // 同じ考え方 —— 規約で禁じるのではなく、表現できなくする)。 + // NULL を心配する必要も無くなった。 + // + // **検証の順序は spec の summary に明記してある: width / height の範囲 -> + // src の handle -> dst の handle。** width / height は buffer の長さでは + // なく、native(cv::dnn::blobFromImage)がその寸法でメモリを確保する + // 引数である。cv::cornerSubPix の win_size で踏んだのと同じ形なので + // 上限を置く(add-abi-function skill の「buffer ではないのに上限が + // 要る引数」)。 + if (width < 1 || width > OCVU_DNN_MAX_BLOB_DIM || + height < 1 || height > OCVU_DNN_MAX_BLOB_DIM) { + return ::ocvu::set_last_error( + OCVU_STATUS_INVALID_ARGUMENT, + "ocvu_dnn_blob_from_image: width and height must be between 1 and OCVU_DNN_MAX_BLOB_DIM"); + } + + const cv::Mat* src_mat = ::ocvu::mat_table_get(src); + if (src_mat == nullptr) { + return ::ocvu::set_last_error(OCVU_STATUS_INVALID_HANDLE, + "ocvu_dnn_blob_from_image: src handle is invalid"); + } + cv::Mat* dst_mat = ::ocvu::mat_table_get(dst); + if (dst_mat == nullptr) { + return ::ocvu::set_last_error(OCVU_STATUS_INVALID_HANDLE, + "ocvu_dnn_blob_from_image: dst handle is invalid"); + } + + // **求めてから dst に入れる。** 失敗経路で dst を途中まで書き換えない + // (ocvu_stereo.cpp / ocvu_calibration.cpp と同じ作法)。 + cv::Mat result; + try { + const cv::Scalar mean_scalar(mean_b, mean_g, mean_r); + result = cv::dnn::blobFromImage(*src_mat, scale, cv::Size(width, height), + mean_scalar, swap_rb != 0, crop != 0); + } catch (const cv::Exception& e) { + return ::ocvu::set_last_error(OCVU_STATUS_OPENCV_ERROR, e.what()); + } + + *dst_mat = result; + return OCVU_STATUS_OK; + OCVU_TRY_END +} + +extern "C" ocvu_status ocvu_dnn_net_forward(ocvu_net_handle net, ocvu_mat_handle input, ocvu_mat_handle output) { + OCVU_TRY_BEGIN + cv::dnn::Net* net_ptr = ::ocvu::net_table_get(net); + if (net_ptr == nullptr) { + return ::ocvu::set_last_error(OCVU_STATUS_INVALID_HANDLE, + "ocvu_dnn_net_forward: net handle is invalid"); + } + const cv::Mat* input_mat = ::ocvu::mat_table_get(input); + if (input_mat == nullptr) { + return ::ocvu::set_last_error(OCVU_STATUS_INVALID_HANDLE, + "ocvu_dnn_net_forward: input handle is invalid"); + } + cv::Mat* output_mat = ::ocvu::mat_table_get(output); + if (output_mat == nullptr) { + return ::ocvu::set_last_error(OCVU_STATUS_INVALID_HANDLE, + "ocvu_dnn_net_forward: output handle is invalid"); + } + + // **求めてから output に入れる。** 失敗経路で output を途中まで + // 書き換えない(ocvu_stereo.cpp / ocvu_calibration.cpp と同じ作法)。 + cv::Mat result; + try { + net_ptr->setInput(*input_mat); + cv::Mat raw = net_ptr->forward(); + if (raw.empty() || raw.dims < 1) { + return ::ocvu::set_last_error(OCVU_STATUS_OPENCV_ERROR, + "ocvu_dnn_net_forward: forward() produced an empty result"); + } + + // **4 次元の出力を 2 次元へ潰す(決定 A)。** 最後の次元を列数として + // 残りをすべて行数へ畳み込む。分類モデルの典型的な (1, N) は + // そのまま 1 x N になる。検出モデルのような (1, C, H, W) では + // N と C の区別が失われ、C*H 行 x W 列になる —— これは + // 「分類の 1 x N を想定している」という契約の帰結である。 + const int last_dim = raw.size[raw.dims - 1]; + if (last_dim <= 0) { + return ::ocvu::set_last_error(OCVU_STATUS_OPENCV_ERROR, + "ocvu_dnn_net_forward: forward() produced a degenerate shape"); + } + const int rows = static_cast(raw.total() / static_cast(last_dim)); + // **`.clone()` が要る(レビュー指摘、Important 2)。** `Net::forward()` + // が返す Mat はネットワーク内部のバッファへの浅いコピーで、 + // `reshape` もヘッダだけを作り直す浅い操作である。`.clone()` を + // 省くと `*output_mat` は net が所有するメモリを指したままになり、 + // 同じ net で 2 回目の forward を呼ぶと 1 回目の出力が黙って + // 書き換わる —— この ABI の他のすべての関数が新しく確保した + // メモリを返しているのに、ここだけが例外になってしまう。 + // **未確認事項**: この所有権の挙動は OpenCV のヘッダには明記が + // 無く、実装から推測している。M7c Task 5 + // (tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs の + // CallingForwardTwiceWithDifferentInputsDoesNotRewriteTheFirstOutput) + // が実物の ONNX で forward を 2 回呼ぶ形の負の対照を試みたが、 + // .clone() を外しても検知できなかった —— 「net が内部バッファを + // 使い回す」という前提そのものが、この規模のモデルでは再現しない + // 可能性が高い(詳細は同テストの docstring)。したがって + // .clone() が必要だと示す再現テストはまだ無いが、`*output_mat` が + // 独立したメモリを持つという契約を成立させているのは .clone() の + // 一行だけなので、消してはならない。 + result = raw.reshape(1, rows).clone(); + } catch (const cv::Exception& e) { + return ::ocvu::set_last_error(OCVU_STATUS_OPENCV_ERROR, e.what()); + } + + *output_mat = result; + return OCVU_STATUS_OK; + OCVU_TRY_END +} diff --git a/native/src/ocvu_dnn_table.cpp b/native/src/ocvu_dnn_table.cpp new file mode 100644 index 0000000..0967223 --- /dev/null +++ b/native/src/ocvu_dnn_table.cpp @@ -0,0 +1,109 @@ +#include "ocvu_dnn_table.h" + +#include +#include +#include +#include + +namespace ocvu { +namespace { + +/* + * cv::dnn::Net を値ではなく間接で持つ。slots は vector なので、伸びるときに + * 要素ごと引っ越す。値で持つと、その引っ越しで net_table_get が過去に + * 返したポインタが全部ぶら下がる —— 壊れるのは add した側ではなく、 + * まったく無関係な handle を使っている側である + * (ocvu_mat_table.cpp / M3 の PR #8 と同じ形)。unique_ptr にすると + * 動くのはポインタだけで、Net 本体のアドレスは固定される。 + */ +struct Slot { + std::unique_ptr net; + uint32_t generation = 1; // 1 から始める。世代 0 の handle は作らない + bool occupied = false; +}; + +/* + * table は関数内 static にする。DLL アンロード時の破棄順序に依存しないため。 + * mutex は Unity のワーカースレッドから同時に呼ばれ得るので必須である。 + */ +struct Table { + std::mutex mutex; + std::vector slots; + std::vector free_indices; +}; + +Table& table() { + static Table instance; + return instance; +} + +constexpr ocvu_net_handle make_handle(uint32_t index, uint32_t generation) { + return (static_cast(generation) << 32) | static_cast(index); +} + +constexpr uint32_t handle_index(ocvu_net_handle h) { return static_cast(h & 0xFFFFFFFFu); } +constexpr uint32_t handle_generation(ocvu_net_handle h) { return static_cast(h >> 32); } + +} // namespace + +ocvu_net_handle net_table_add(std::unique_ptr net) { + Table& t = table(); + std::lock_guard lock(t.mutex); + + uint32_t index; + if (!t.free_indices.empty()) { + index = t.free_indices.back(); + t.free_indices.pop_back(); + } else { + t.slots.emplace_back(); + index = static_cast(t.slots.size() - 1); + } + + Slot& slot = t.slots[index]; + slot.net = std::move(net); + slot.occupied = true; + return make_handle(index, slot.generation); +} + +cv::dnn::Net* net_table_get(ocvu_net_handle handle) { + if (handle == 0) { return nullptr; } + + Table& t = table(); + std::lock_guard lock(t.mutex); + + const uint32_t index = handle_index(handle); + if (index >= t.slots.size()) { return nullptr; } + + Slot& slot = t.slots[index]; + if (!slot.occupied || slot.generation != handle_generation(handle)) { return nullptr; } + return slot.net.get(); +} + +size_t net_table_slot_capacity() { + Table& t = table(); + std::lock_guard lock(t.mutex); + return t.slots.capacity(); +} + +bool net_table_remove(ocvu_net_handle handle) { + if (handle == 0) { return false; } + + Table& t = table(); + std::lock_guard lock(t.mutex); + + const uint32_t index = handle_index(handle); + if (index >= t.slots.size()) { return false; } + + Slot& slot = t.slots[index]; + if (!slot.occupied || slot.generation != handle_generation(handle)) { return false; } + + slot.net.reset(); + slot.occupied = false; + // 世代を進めることで、この索引の古い handle は以後すべて無効になる。 + // 一周して衝突するのは 2^32 回の解放後であり、実用上到達しない。 + ++slot.generation; + t.free_indices.push_back(index); + return true; +} + +} // namespace ocvu diff --git a/native/src/ocvu_dnn_table.h b/native/src/ocvu_dnn_table.h new file mode 100644 index 0000000..3e0346b --- /dev/null +++ b/native/src/ocvu_dnn_table.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include "opencv_unity_native.h" + +namespace ocvu { + +// **Slot は値ではなく unique_ptr を持つ。** +// +// 値で持つと、表が伸びたときに再配置が起き、先に解決したポインタが +// 全部ぶら下がる。**壊れるのは伸ばした側ではなく、無関係な handle を +// 使っている側**である(M3 の PR #8 で mat_table が実際に踏んだ)。 +ocvu_net_handle net_table_add(std::unique_ptr net); + +// 無効な handle には nullptr を返す。**落とさない。** +cv::dnn::Net* net_table_get(ocvu_net_handle handle); + +// 解放できたら true。既に解放済み・未知なら false。**落とさない。** +bool net_table_remove(ocvu_net_handle handle); + +// table の内部配列が確保している容量を返す(テスト用)。 +// +// **要素数ではなく容量である。** ocvu_mat_table.h の同名関数とまったく +// 同じ理由で要る —— 「4096 個足せば再配置されるだろう」という前提に頼らず、 +// 再配置が実際に起きたことを容量の変化で測る。test_dnn.cpp が +// net_table の 2 人目の利用者になった時点で、この table が +// test_dnn_table_stability.cpp 専用(=常に空から始まる)という前提は +// 崩れる。容量を実測しないと、その回の実行だけ偶然 4096 個で再配置が +// 起きず、「何も検証していないテスト」が静かに緑になり得る。 +size_t net_table_slot_capacity(); + +} // namespace ocvu diff --git a/native/src/ocvu_mat.cpp b/native/src/ocvu_mat.cpp index 6ef7fc7..aee6465 100644 --- a/native/src/ocvu_mat.cpp +++ b/native/src/ocvu_mat.cpp @@ -121,6 +121,18 @@ extern "C" ocvu_status ocvu_mat_get_info(ocvu_mat_handle handle, ocvu_mat_info* if (mat == nullptr) { return ::ocvu::set_last_error(OCVU_STATUS_INVALID_HANDLE, "handle is invalid"); } + // **2 次元より多い次元を持つ Mat を素通ししない(レビュー指摘、Important 1)。** + // ocvu_dnn_blob_from_image が作る Mat は dims == 4 でありうる。この struct は + // rows / cols / channels / step という 2 次元の語彙しか持たないので、 + // dims > 2 のとき OpenCV は rows = cols = -1 を返す —— 素通しすると + // OCVU_STATUS_OK のまま rows = -1、total_bytes が負の値になる。 + // 「読めない」ではなく「間違った値を自信満々で返す」形だったので、 + // ここで明示的に断る。 + if (mat->dims > 2) { + return ::ocvu::set_last_error( + OCVU_STATUS_INVALID_ARGUMENT, + "ocvu_mat_get_info: mat has more than 2 dimensions (e.g. a dnn blob)"); + } out_info->rows = mat->rows; out_info->cols = mat->cols; out_info->type = from_cv_type(mat->type()); diff --git a/native/tests/CMakeLists.txt b/native/tests/CMakeLists.txt index 1db0e37..c2a671c 100644 --- a/native/tests/CMakeLists.txt +++ b/native/tests/CMakeLists.txt @@ -34,6 +34,8 @@ add_executable(ocvu_tests test_core_ops.cpp test_matching.cpp test_stereo.cpp + test_dnn_table_stability.cpp + test_dnn.cpp ) # SHARED ではなく STATIC 側にリンクする(native/CMakeLists.txt の注記を参照) diff --git a/native/tests/test_dnn.cpp b/native/tests/test_dnn.cpp new file mode 100644 index 0000000..eae3539 --- /dev/null +++ b/native/tests/test_dnn.cpp @@ -0,0 +1,173 @@ +// **有効な ONNX を手で組むのは現実的でない。** だからこのテストは +// **壊れた入力に対する振る舞い**を固定する。正常系は L3 が、 +// 実物の小さなモデルを使って見る(Task 5)。 +#include +#include +#include +#include + +#include + +// **内部 table への直接アクセス。** `ocvu_dnn_net_forward` の入口検証 +// (net -> input -> output の順)を試すには「有効な net handle」が要るが、 +// 実物の ONNX が無いので `ocvu_dnn_net_read_onnx` では作れない。 +// `net_table_add` で空の `cv::dnn::Net` を直接 table に入れる —— +// `native/tests/test_dnn_table_stability.cpp` が同じ table に対して +// 行っている white-box アクセスと同じ形である。handle の有効性は +// generation + 索引だけで決まるので、中身が空でも「有効な net handle」 +// として機能する。 +#include "ocvu_dnn_table.h" +#include "opencv_unity_native.h" + +TEST(Dnn, ReadingGarbageAsOnnxFailsWithoutCrashing) { + std::vector garbage(64, 0xAB); + ocvu_net_handle handle = 0; + + ocvu_status st = ocvu_dnn_net_read_onnx( + garbage.data(), static_cast(garbage.size()), &handle); + + // **OPENCV_ERROR を要求する。** + // OCVU_TRY_END は cv::Exception を UNKNOWN_ERROR に変換するので、 + // この関数は自分で catch (const cv::Exception&) を書かなければならない。 + EXPECT_EQ(st, OCVU_STATUS_OPENCV_ERROR); + EXPECT_EQ(handle, 0u) << "失敗したのに handle が書かれた"; +} + +TEST(Dnn, ANullBufferIsRejected) { + ocvu_net_handle handle = 0; + EXPECT_EQ(ocvu_dnn_net_read_onnx(nullptr, 16, &handle), OCVU_STATUS_NULL_POINTER); + EXPECT_EQ(handle, 0u); +} + +TEST(Dnn, ANullOutHandleIsRejected) { + std::vector bytes(16, 0); + EXPECT_EQ(ocvu_dnn_net_read_onnx(bytes.data(), 16, nullptr), + OCVU_STATUS_NULL_POINTER); +} + +TEST(Dnn, ANegativeOrZeroLengthIsRejected) { + std::vector bytes(16, 0); + ocvu_net_handle handle = 0; + EXPECT_EQ(ocvu_dnn_net_read_onnx(bytes.data(), 0, &handle), + OCVU_STATUS_INVALID_ARGUMENT); + EXPECT_EQ(ocvu_dnn_net_read_onnx(bytes.data(), -1, &handle), + OCVU_STATUS_INVALID_ARGUMENT); + EXPECT_EQ(handle, 0u); +} + +TEST(Dnn, ReleasingAnUnknownHandleIsRejectedWithoutCrashing) { + EXPECT_EQ(ocvu_dnn_net_release(0), OCVU_STATUS_INVALID_HANDLE); + EXPECT_EQ(ocvu_dnn_net_release(0xDEADBEEF), OCVU_STATUS_INVALID_HANDLE); +} + +TEST(Dnn, ForwardOnAnUnknownHandleIsRejected) { + ocvu_mat_handle input = 0, output = 0; + ASSERT_EQ(ocvu_mat_create(1, 1, OCVU_MAT_TYPE_32FC1, &input), OCVU_STATUS_OK); + ASSERT_EQ(ocvu_mat_create(1, 1, OCVU_MAT_TYPE_32FC1, &output), OCVU_STATUS_OK); + + EXPECT_EQ(ocvu_dnn_net_forward(0xDEADBEEF, input, output), + OCVU_STATUS_INVALID_HANDLE); + + ocvu_mat_release(input); + ocvu_mat_release(output); +} + +// **blob の寸法は呼ぶ側が渡す int32_t で、OpenCV の中で寸法になる。** +// cv::cornerSubPix の win_size で踏んだのと同じ形なので、上限を置く +// (add-abi-function skill の「buffer ではないのに上限が要る引数」)。 +TEST(Dnn, AnAbsurdBlobSizeIsRejectedRatherThanAttempted) { + ocvu_mat_handle src = 0, blob = 0; + ASSERT_EQ(ocvu_mat_create(8, 8, OCVU_MAT_TYPE_8UC3, &src), OCVU_STATUS_OK); + ASSERT_EQ(ocvu_mat_create(1, 1, OCVU_MAT_TYPE_32FC1, &blob), OCVU_STATUS_OK); + + EXPECT_EQ( + ocvu_dnn_blob_from_image(src, blob, 1.0, 1 << 20, 1 << 20, 0.0, 0.0, 0.0, 0, 0), + OCVU_STATUS_INVALID_ARGUMENT); + EXPECT_EQ( + ocvu_dnn_blob_from_image(src, blob, 1.0, 0, 8, 0.0, 0.0, 0.0, 0, 0), + OCVU_STATUS_INVALID_ARGUMENT); + + ocvu_mat_release(src); + ocvu_mat_release(blob); +} + +TEST(Dnn, BlobFromImageRejectsInvalidSrcHandle) { + ocvu_mat_handle dst = 0; + ASSERT_EQ(ocvu_mat_create(1, 1, OCVU_MAT_TYPE_32FC1, &dst), OCVU_STATUS_OK); + + EXPECT_EQ( + ocvu_dnn_blob_from_image(0xDEADBEEF, dst, 1.0, 8, 8, 0.0, 0.0, 0.0, 0, 0), + OCVU_STATUS_INVALID_HANDLE); + + ocvu_mat_release(dst); +} + +TEST(Dnn, BlobFromImageRejectsInvalidDstHandle) { + ocvu_mat_handle src = 0; + ASSERT_EQ(ocvu_mat_create(8, 8, OCVU_MAT_TYPE_8UC3, &src), OCVU_STATUS_OK); + + EXPECT_EQ( + ocvu_dnn_blob_from_image(src, 0xDEADBEEF, 1.0, 8, 8, 0.0, 0.0, 0.0, 0, 0), + OCVU_STATUS_INVALID_HANDLE); + + ocvu_mat_release(src); +} + +// **検証の順序をロックする。** spec の summary は「width / height の範囲を +// 先に見る」と約束している。無効な src/dst handle と範囲外の width を +// 同時に渡し、返るのが OCVU_STATUS_INVALID_HANDLE ではなく +// OCVU_STATUS_INVALID_ARGUMENT であることを見る —— これが崩れたら、 +// 順序を変えた変更がここで初めて赤くなる(レビュー指摘 M1)。 +TEST(Dnn, BlobFromImageChecksDimensionsBeforeHandles) { + EXPECT_EQ( + ocvu_dnn_blob_from_image(0xDEADBEEF, 0xDEADBEEF, 1.0, 0, 8, 0.0, 0.0, 0.0, 0, 0), + OCVU_STATUS_INVALID_ARGUMENT); +} + +TEST(Dnn, ForwardRejectsInvalidInputHandle) { + const ocvu_net_handle net = ::ocvu::net_table_add(std::make_unique()); + ocvu_mat_handle output = 0; + ASSERT_EQ(ocvu_mat_create(1, 1, OCVU_MAT_TYPE_32FC1, &output), OCVU_STATUS_OK); + + EXPECT_EQ(ocvu_dnn_net_forward(net, 0xDEADBEEF, output), OCVU_STATUS_INVALID_HANDLE); + + ocvu_mat_release(output); + ::ocvu::net_table_remove(net); +} + +TEST(Dnn, ForwardRejectsInvalidOutputHandle) { + const ocvu_net_handle net = ::ocvu::net_table_add(std::make_unique()); + ocvu_mat_handle input = 0; + ASSERT_EQ(ocvu_mat_create(1, 1, OCVU_MAT_TYPE_32FC1, &input), OCVU_STATUS_OK); + + EXPECT_EQ(ocvu_dnn_net_forward(net, input, 0xDEADBEEF), OCVU_STATUS_INVALID_HANDLE); + + ocvu_mat_release(input); + ::ocvu::net_table_remove(net); +} + +// **これが Unity の実際の壊れ方に一番近い**(`OnDestroy` が net を解放し、 +// 生き残ったコルーチンが 1 回だけ forward を呼ぶ)。`ocvu_dnn_net_release` +// という公開関数で解放してから forward を呼ぶ、実際の使用順序をそのまま +// 再現する。 +// +// **ここで使う net は実物の ONNX から読んだものではない**(`net_table_add` +// で直接作った空の `cv::dnn::Net`)。handle の有効性は generation と索引 +// だけで決まり、中身が空か読み込み済みかには依らないので、この検査で +// 見ている「解放後は INVALID_HANDLE になる」という性質は実物の net でも +// 変わらないはずである —— ただし forward() が実際に走る経路 +// (setInput 以降)まではここでは実証していない。実物の ONNX を使った +// 検証は後続タスクの担当。 +TEST(Dnn, ForwardOnAReleasedNetIsRejectedWithoutCrashing) { + const ocvu_net_handle net = ::ocvu::net_table_add(std::make_unique()); + ASSERT_EQ(ocvu_dnn_net_release(net), OCVU_STATUS_OK); + + ocvu_mat_handle input = 0, output = 0; + ASSERT_EQ(ocvu_mat_create(1, 1, OCVU_MAT_TYPE_32FC1, &input), OCVU_STATUS_OK); + ASSERT_EQ(ocvu_mat_create(1, 1, OCVU_MAT_TYPE_32FC1, &output), OCVU_STATUS_OK); + + EXPECT_EQ(ocvu_dnn_net_forward(net, input, output), OCVU_STATUS_INVALID_HANDLE); + + ocvu_mat_release(input); + ocvu_mat_release(output); +} diff --git a/native/tests/test_dnn_table_stability.cpp b/native/tests/test_dnn_table_stability.cpp new file mode 100644 index 0000000..ee092f4 --- /dev/null +++ b/native/tests/test_dnn_table_stability.cpp @@ -0,0 +1,90 @@ +// **2 つ目の handle 表が、1 つ目と同じ欠陥を持たないことを決定的に固定する。** +// +// M3 の PR #8 で mat_table が踏んだ形: Slot が値でオブジェクトを持ち、 +// get が配列内部のポインタを返すと、別スレッドの add で配列が伸びたときに +// **先に解決したポインタが全部ぶら下がる。** +// +// **ローカル 3 回と直前 3 回の CI が緑で、1 度だけ落ちた。** +// フレークとして再実行していたら残っていた。だからここでは +// **確率に頼らず、伸びを強制してから古いポインタを触る。** +// +// **「4096 個足せば再配置されるだろう」に頼らない。** これが成立していたのは +// この table を使うのがこのファイルだけだったからで、Task 3 が +// native/tests/test_dnn.cpp から同じ net_table を使い始めた時点で崩れる +// 前提になった(native/tests/test_mat_table_stability.cpp の +// ResolvedPointerSurvivesTableGrowth と同じ形。あちらの comment も +// 同じ理由で「足りるだろう」を拒んでいる)。容量(要素数ではない)を +// 前後で測り、実際に増えたことまで確かめる。 +#include +#include +#include +#include + +#include "ocvu_dnn_table.h" + +TEST(DnnTableStability, APointerStaysValidWhileTheTableGrows) { + // 1 つ確保して、そのポインタを先に解決しておく。 + auto first = ocvu::net_table_add(std::make_unique()); + cv::dnn::Net* resolved = ocvu::net_table_get(first); + ASSERT_NE(resolved, nullptr); + + const size_t capacity_before = ocvu::net_table_slot_capacity(); + + // 表を大きく伸ばす。**vector が値を持っていれば、ここで再配置が起きる。** + std::vector others; + for (int i = 0; i < 4096; ++i) { + others.push_back(ocvu::net_table_add(std::make_unique())); + } + + const size_t capacity_after = ocvu::net_table_slot_capacity(); + ASSERT_GT(capacity_after, capacity_before) + << "table の内部配列が 1 度も再配置されなかったので、この test は" + "何も検証していない(容量 " << capacity_before << " のまま)。" + "確保する数を増やすか、他のテストが free list をこの回だけ" + "偶然使い切っていないか確かめること。"; + + // **先に解決したポインタがまだ生きていること。** + EXPECT_EQ(ocvu::net_table_get(first), resolved); + EXPECT_TRUE(resolved->empty()); // 触って落ちないこと + + for (auto h : others) { ocvu::net_table_remove(h); } + EXPECT_TRUE(ocvu::net_table_remove(first)); +} + +TEST(DnnTableStability, ConcurrentAddAndGetDoNotCorruptEachOther) { + auto mine = ocvu::net_table_add(std::make_unique()); + cv::dnn::Net* resolved = ocvu::net_table_get(mine); + ASSERT_NE(resolved, nullptr); + + std::thread grower([] { + std::vector hs; + for (int i = 0; i < 2048; ++i) { + hs.push_back(ocvu::net_table_add(std::make_unique())); + } + for (auto h : hs) { ocvu::net_table_remove(h); } + }); + + // **自分の handle だけを触る。これが「正しい使い方」である。** + for (int i = 0; i < 2048; ++i) { + cv::dnn::Net* again = ocvu::net_table_get(mine); + ASSERT_EQ(again, resolved); + ASSERT_TRUE(again->empty()); + } + grower.join(); + EXPECT_TRUE(ocvu::net_table_remove(mine)); +} + +TEST(DnnTableStability, AReleasedHandleResolvesToNull) { + auto h = ocvu::net_table_add(std::make_unique()); + EXPECT_NE(ocvu::net_table_get(h), nullptr); + EXPECT_TRUE(ocvu::net_table_remove(h)); + EXPECT_EQ(ocvu::net_table_get(h), nullptr); + + // **二重解放は落とさず false を返す。** + EXPECT_FALSE(ocvu::net_table_remove(h)); +} + +TEST(DnnTableStability, AnUnknownHandleResolvesToNull) { + EXPECT_EQ(ocvu::net_table_get(0), nullptr); + EXPECT_EQ(ocvu::net_table_get(0xDEADBEEF), nullptr); +} diff --git a/native/tests/test_mat_lifecycle.cpp b/native/tests/test_mat_lifecycle.cpp index 8eb9765..4e08001 100644 --- a/native/tests/test_mat_lifecycle.cpp +++ b/native/tests/test_mat_lifecycle.cpp @@ -1,5 +1,8 @@ #include +#include + +#include "ocvu_mat_table.h" #include "opencv_unity_native.h" TEST(MatLifecycle, CreateProducesAUsableHandle) { @@ -136,3 +139,36 @@ TEST(MatLifecycle, AnUnknownMatTypeIsRejected) { EXPECT_EQ(ocvu_mat_create(2, 3, -1, &h), OCVU_STATUS_INVALID_ARGUMENT); EXPECT_EQ(h, OCVU_MAT_HANDLE_NONE) << "断ったのに out_handle を書いている"; } + +// **2 次元より多い次元を持つ Mat が table に入りうる**(`ocvu_dnn_blob_from_image` +// が実際にそういう Mat を作る。レビュー指摘、Important 1)。この ABI の +// `ocvu_mat_info` は rows / cols / channels / step しか持たず、2 次元を +// 前提にしている。 +// +// **`ocvu_mat_create` はこの経路を通らない**(rows/cols しか受けないので +// 3 次元以上を作れない)ので、ここでは table を直接使って合成する —— +// `native/tests/test_dnn_table_stability.cpp` が `net_table` に対して +// 行っているのと同じ、内部 table への直接アクセスによる white-box テストで +// ある。 +// +// **壊れ方の実測**: この検査を足す前は、OpenCV が dims > 2 のとき +// `rows = cols = -1` を返すのをそのまま素通しし、`ocvu_mat_get_info` は +// `OCVU_STATUS_OK` で `rows = -1` / `total_bytes` が負の値を書いていた —— +// 「読めない」ではなく「間違った値を自信満々で返す」形だった。 +TEST(MatLifecycle, GetInfoRejectsMatsWithMoreThanTwoDimensions) { + const int sizes[3] = {2, 3, 4}; + cv::Mat blob(3, sizes, CV_32FC1); + ASSERT_EQ(blob.dims, 3); + + const ocvu_mat_handle h = ::ocvu::mat_table_acquire(blob); + ASSERT_NE(h, OCVU_MAT_HANDLE_NONE); + + ocvu_mat_info info{}; + // わざと汚しておく —— 失敗経路で書き換えていないことを見分けるため + // (imgcodecs の `out_required_size` と同じ考え方)。 + info.rows = 12345; + EXPECT_EQ(ocvu_mat_get_info(h, &info), OCVU_STATUS_INVALID_ARGUMENT); + EXPECT_EQ(info.rows, 12345) << "拒んだのに out_info を書き換えている"; + + EXPECT_EQ(ocvu_mat_release(h), OCVU_STATUS_OK); +} diff --git a/native/tests/test_module_linkage.cpp b/native/tests/test_module_linkage.cpp index 64e710b..dc04c57 100644 --- a/native/tests/test_module_linkage.cpp +++ b/native/tests/test_module_linkage.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -152,3 +153,14 @@ TEST(ModuleLinkage, StereoIsLinked) { EXPECT_EQ(matcher->getNumDisparities(), 16); EXPECT_EQ(matcher->getBlockSize(), 21); } + +TEST(ModuleLinkage, DnnIsLinked) { + // **COMPONENTS に足すだけでは binary は 1 バイトも増えない。** + // 静的リンクは参照された object しか引かないので、 + // **cv::dnn:: を実際に参照するテストだけが「リンクした」の証拠になる** + // (M3.5 と M5 で 2 度実測した)。dnn は Task 2 で COMPONENTS に足したが + // (unique_ptr の破棄に定義が要るため)、cv::dnn::Net を + // 構築・参照するのはこのテストが最初である。 + cv::dnn::Net net; + EXPECT_TRUE(net.empty()); +} diff --git a/tests/Managed/CvUnity.Runtime.Shim/CvUnity.Runtime.Shim.csproj b/tests/Managed/CvUnity.Runtime.Shim/CvUnity.Runtime.Shim.csproj index 40a07fc..d4cab28 100644 --- a/tests/Managed/CvUnity.Runtime.Shim/CvUnity.Runtime.Shim.csproj +++ b/tests/Managed/CvUnity.Runtime.Shim/CvUnity.Runtime.Shim.csproj @@ -19,9 +19,15 @@ + UnityIntegration は意図的に含めない。 + + Interop.Dnn / Dnn は Unity 側では OCVU_PROFILE_DNN の defineConstraints + で切れるが、この .csproj はそれを見ない —— L3 は常に dnn を見る + (CLAUDE.md「L3 のクラッシュ・ハング耐性」の隣、M7c Task 4 の意図どおり)。 --> + + diff --git a/tests/Managed/CvUnity.Tests.Managed/CvUnity.Tests.Managed.csproj b/tests/Managed/CvUnity.Tests.Managed/CvUnity.Tests.Managed.csproj index 17a7d4e..74debe6 100644 --- a/tests/Managed/CvUnity.Tests.Managed/CvUnity.Tests.Managed.csproj +++ b/tests/Managed/CvUnity.Tests.Managed/CvUnity.Tests.Managed.csproj @@ -33,4 +33,11 @@ + + + + + diff --git a/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs new file mode 100644 index 0000000..13d25ff --- /dev/null +++ b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs @@ -0,0 +1,261 @@ +using System; +using System.IO; +using CvUnity; +using CvUnity.Dnn; +using Xunit; + +/// +/// **実物の ONNX を通して、境界の往復が成立することを見る。** +/// +/// tiny.onnx(TestModels/ 隣の tiny.onnx.py が生成した合成モデル。 +/// **外部から取ってきたものではない** —— 詳細はそのスクリプトの docstring を +/// 見ること)は Identity 1 ノードなので、推論の中身は何もしない —— +/// **確かめているのは「読めて、blob になって、forward が返る」ことである。** +/// 推論そのものの正しさは OpenCV の責任であって、この境界の責任ではない。 +/// +public class DnnInferenceTests +{ + private const int ModelSize = 4; + + private static byte[] TinyModel() + { + var path = Path.Combine( + AppContext.BaseDirectory, "TestModels", "tiny.onnx"); + Assert.True(File.Exists(path), $"テスト用のモデルが無い: {path}"); + return File.ReadAllBytes(path); + } + + [Fact] + public void AValidOnnxLoads() + { + using var net = CvDnn.ReadOnnx(TinyModel()); + Assert.NotNull(net); + } + + /// + /// blob は 4 次元のまま保持される契約(spec の summary、決定 A の前段) + /// なので、2 次元前提の はこの handle を + /// 例外で拒む —— それ自体がこの契約の検証であり、値は読めない。 + /// 実際の要素数は を通した 2 次元の出力側で見る + /// ()。 + /// + [Fact] + public void BlobFromImageKeepsTheBlobFourDimensional() + { + using var src = CvMat.Create(8, 8, CvMatType.Bgr24); + using var blob = CvMat.Create(1, 1, CvMatType.Response32); + + CvDnn.BlobFromImage( + src, blob, scale: 1.0 / 255.0, width: ModelSize, height: ModelSize, + mean: new[] { 0.0, 0.0, 0.0 }, swapRb: false, crop: false); + + var ex = Assert.Throws(() => { _ = blob.Rows; }); + Assert.Equal(CvStatus.InvalidArgument, ex.Status); + } + + [Fact] + public void ForwardReturnsTheInputUnchangedForAnIdentityNetwork() + { + using var net = CvDnn.ReadOnnx(TinyModel()); + using var src = CvMat.Create(ModelSize, ModelSize, CvMatType.Bgr24); + using var blob = CvMat.Create(1, 1, CvMatType.Response32); + using var output = CvMat.Create(1, 1, CvMatType.Response32); + + CvDnn.BlobFromImage( + src, blob, scale: 1.0, width: ModelSize, height: ModelSize, + mean: new[] { 0.0, 0.0, 0.0 }, swapRb: false, crop: false); + + CvDnn.Forward(net, blob, output); + + // 1 (N) x 3 (C) x 4 (H) x 4 (W) = 48 要素。Identity なので個数は変わらない。 + Assert.Equal(1 * 3 * ModelSize * ModelSize, output.Rows * output.Cols); + } + + /// + /// **解放した net で forward を呼んでも落ちないこと。** + /// の getter が + /// を投げるので、native へは届かない —— mat_table と同じ「解放後アクセスは + /// 例外で返る」規約を、C# 側の一番外側で確かめる。 + /// + [Fact] + public void ForwardOnADisposedNetIsRejected() + { + var net = CvDnn.ReadOnnx(TinyModel()); + net.Dispose(); + + using var input = CvMat.Create(1, 48, CvMatType.Response32); + using var output = CvMat.Create(1, 1, CvMatType.Response32); + + Assert.Throws(() => CvDnn.Forward(net, input, output)); + } + + /// + /// **C# レベルでの二重 Dispose。** のガードは + /// と同じ形(handle が 0 なら何もしない)だが、 + /// 「同じ形をしている」ことは「動くことが確かめられている」ことと同じでは + /// ない —— DnnTests.DisposingTwiceIsSafe は有効な net を作れなかった + /// ので NativeMethodsDnn.ocvu_dnn_net_release を直接叩いていた。 + /// ここでは実物の を実際に二重解放する。 + /// + [Fact] + public void DisposingACvNetTwiceIsSafe() + { + var net = CvDnn.ReadOnnx(TinyModel()); + net.Dispose(); + net.Dispose(); // 二重 Dispose は例外にならない(IDisposable の規約) + } + + /// + /// ocvu_dnn_net_forward は net の内部バッファではなく独立したコピーを + /// 返す契約(docs/api-reference.md、spec の summary)を、実物のモデルで + /// 確かめる。2 回の forward には別々の入力を使う —— 同じ blob を 2 回渡すと + /// (defect があってもなくても)1 回目と 2 回目の値が一致してしまい、 + /// 何も検証できない(最初の実装がまさにこれだった)。 + /// + /// この negative control は取れなかった(prove-a-check-works)。 + /// Task 3 の .clone()(native/src/ocvu_dnn.cpp)を一時的に外し、 + /// このネットワーク(Identity 1 ノード)でも 2 層の Relu(learned weight + /// を持たない別モデルで検証)でもこのテストを走らせたが、**どちらも + /// このテストは緑のままだった** —— 外した状態と戻した状態とで結果が + /// 変わらない。 + /// + /// + /// 「mat_table 側の参照カウントが守っている」という説は、ほぼ確実に + /// 誤りである。 cv::Mat::create() には、要求された shape と type + /// が現在のヘッダと一致するときに早期リターンする経路があり、**そこは + /// 参照カウントを一切見ない。** つまり net が持続的なバッファを使い回して + /// いたとしても、外側(mat_table の Slot)が result への参照を生かして + /// いることは、2 回目の forward による in-place の上書きを止める理由には + /// ならない —— 参照カウントは「守ってくれる仕組み」ではなかった。 + /// + /// + /// **有力なのは、OpenCV 5 の新しい dnn engine が forward() のたびに + /// 新しく確保したバッファを返している、という説である。** これも + /// 未確認(復元済みツリーには header しか無く、OpenCV 本体のソースを + /// 読める状態ではない)だが、上の refcount 説のように機構として + /// 否定されているわけではないので、こちらを主説として置く。 + /// + /// + /// **この結果、`.clone()` の位置づけは「念のため」ではない。** 元々の + /// コメントは「参照カウントが本来なら守ってくれるところを、念のため + /// 独立コピーにしておく」という書き方に読めたが、それは違う —— + /// refcount 説が否定された以上、**forward() が返す Mat が net 側の + /// バッファを指していないという保証はどこにも無く、`.clone()` こそが + /// 「返された handle が独立したメモリを持つ」という契約を成立させている + /// 唯一のものである。** 消してよい安全装置ではない。 + /// + /// + /// **したがってこのテストは、想定した defect を実際に再現させて + /// 潰したことの証明にはなっていない** —— 契約どおりの結果を実物の + /// モデルで確認した positive な回帰検査ではあるが、`.clone()` を + /// 消しても今のところこのテストは検知できない。同じ形の負の対照が + /// 取れないことがある、という M6 の教訓(CLAUDE.md + /// 「prove-a-check-works」の節)をここでも踏んだ。 + /// + /// + [Fact] + public void CallingForwardTwiceWithDifferentInputsDoesNotRewriteTheFirstOutput() + { + using var net = CvDnn.ReadOnnx(TinyModel()); + using var srcA = CvMat.Create(ModelSize, ModelSize, CvMatType.Bgr24); + using var srcB = CvMat.Create(ModelSize, ModelSize, CvMatType.Bgr24); + FillWithConstant(srcA, 10); + FillWithConstant(srcB, 200); + + using var blobA = CvMat.Create(1, 1, CvMatType.Response32); + using var blobB = CvMat.Create(1, 1, CvMatType.Response32); + using var firstOutput = CvMat.Create(1, 1, CvMatType.Response32); + using var secondOutput = CvMat.Create(1, 1, CvMatType.Response32); + + var mean = new[] { 0.0, 0.0, 0.0 }; + CvDnn.BlobFromImage(srcA, blobA, scale: 1.0, width: ModelSize, height: ModelSize, + mean: mean, swapRb: false, crop: false); + CvDnn.BlobFromImage(srcB, blobB, scale: 1.0, width: ModelSize, height: ModelSize, + mean: mean, swapRb: false, crop: false); + + CvDnn.Forward(net, blobA, firstOutput); + var afterFirstForward = ReadAll(firstOutput); + + // **同じ net で 2 回目の forward を呼ぶ。** 別の handle + // (secondOutput) に書かせるが、defect が再発していれば + // net が内部で持つバッファ経由で firstOutput まで書き換わる。 + CvDnn.Forward(net, blobB, secondOutput); + var afterSecondForward = ReadAll(firstOutput); + + Assert.Equal(afterFirstForward, afterSecondForward); + + // **前提の健全性チェック。** srcA と srcB が実際に異なる結果を + // 生んでいなければ、上の Assert.Equal は defect の有無に関わらず + // 常に真になり、何も検証したことにならない。 + var secondOutputBytes = ReadAll(secondOutput); + Assert.NotEqual(afterFirstForward, secondOutputBytes); + } + + /// + /// **`ocvu_dnn_net_forward` の `catch (const cv::Exception&)` 経路 + /// (`native/src/ocvu_dnn.cpp` の forward 実装、`OCVU_STATUS_OPENCV_ERROR` + /// を返す意図的な分岐)を、実物のモデルで踏む(レビュー指摘 M6)。** + /// このファイルの他のテストはすべて が + /// 作った正しい形の blob しか渡していないので、この経路はハッピーケース + /// の外で一度も実行されていなかった —— これは攻撃者が影響できる入力に + /// 対するエラー処理であり、未実行のまま配ってはならない。 + /// + /// **当初は「空間サイズを 4x4 から 8x8 に変える」形で書いたが、これは + /// 例外を投げなかった**(実測: `output.Rows * output.Cols` が + /// `24 * 8 = 192` になり forward が成功した)。`tiny.onnx` は + /// `Identity` 1 ノードだけのネットワークで、OpenCV の dnn engine は + /// 実行時の入力から shape を再推論するため、ONNX の value_info に + /// 書かれた `[1, 3, 4, 4]` という宣言は検証されない——空間サイズの + /// 不一致では、この model に対して負の対照が取れなかった + /// (`.clone()` のときと同じ形。docs/abi-ownership-and-versioning.md + /// §1.7 参照)。 + /// + /// + /// **代わりに型の不一致で踏めることを確かめた。** + /// を経由しない生の 8bit 3channel Mat(CV_8UC3)をそのまま + /// に渡すと、`net.setInput()` が + /// incompatible type of input tensor #0: CV_8UC3 given, CV_32FC1 + /// expected という `cv::Exception` を実際に投げ、 + /// `OCVU_STATUS_OPENCV_ERROR` として返ってくる。**assert は完全一致では + /// なく `CV_8UC3` / `CV_32FC1` の部分一致(Assert.Contains)に + /// している** —— OpenCV の点リリースが文言を書き換えても、型名が + /// 変わらない限り検査が生き残るようにするための意図的な選択である。 + /// **呼ぶ側が `BlobFromImage` を通さずに `Forward` を呼ぶ**という、 + /// 空間サイズの取り違えより現実的な誤用でもある。 + /// + /// + [Fact] + public void ForwardWithARawNonBlobMatReturnsAnOpenCvError() + { + using var net = CvDnn.ReadOnnx(TinyModel()); + // BlobFromImage を経由しない、8bit 3channel の素の Mat をそのまま渡す + // (dnn engine が要求する CV_32FC1 の blob になっていない)。 + using var notABlob = CvMat.Create(ModelSize, ModelSize, CvMatType.Bgr24); + using var output = CvMat.Create(1, 1, CvMatType.Response32); + + var ex = Assert.Throws(() => CvDnn.Forward(net, notABlob, output)); + Assert.Equal(CvStatus.OpenCvError, ex.Status); + Assert.Contains("CV_8UC3", ex.Message); + Assert.Contains("CV_32FC1", ex.Message); + } + + /// 32FC1(4 byte/pixel、1 channel)の Mat を丸ごと byte[] に読み出す。 + private static byte[] ReadAll(CvMat mat) + { + const int bytesPerElement = 4; + long stride = mat.Cols * bytesPerElement; + var buffer = new byte[stride * mat.Rows]; + mat.CopyTo(buffer, stride); + return buffer; + } + + /// 8UC3(Bgr24)の Mat を、全画素・全チャンネルが同じ値になるよう埋める。 + private static void FillWithConstant(CvMat mat, byte value) + { + const int channels = 3; + long stride = mat.Cols * channels; + var buffer = new byte[stride * mat.Rows]; + for (int i = 0; i < buffer.Length; i++) { buffer[i] = value; } + mat.CopyFrom(buffer, stride); + } +} diff --git a/tests/Managed/CvUnity.Tests.Managed/DnnTests.cs b/tests/Managed/CvUnity.Tests.Managed/DnnTests.cs new file mode 100644 index 0000000..9d23b59 --- /dev/null +++ b/tests/Managed/CvUnity.Tests.Managed/DnnTests.cs @@ -0,0 +1,45 @@ +using System; +using CvUnity; +using CvUnity.Dnn; +using Xunit; + +/// +/// **有効な ONNX を手で組むのは現実的でない。** +/// ここが見るのは、壊れた入力・寿命・所有権である。 +/// **正常系は が、実物の小さなモデル +/// (tiny.onnx)を使って見ている**(Task 5 で追加済み)。 +/// +public class DnnTests +{ + [Fact] + public void ReadingGarbageThrowsRatherThanReturningABrokenNet() + { + var garbage = new byte[64]; + for (int i = 0; i < garbage.Length; i++) { garbage[i] = 0xAB; } + + var ex = Assert.Throws(() => CvDnn.ReadOnnx(garbage)); + Assert.Equal(CvStatus.OpenCvError, ex.Status); + } + + [Fact] + public void ANullBufferIsRejected() + => Assert.Throws(() => CvDnn.ReadOnnx(null)); + + [Fact] + public void AnEmptyBufferIsRejected() + => Assert.Throws(() => CvDnn.ReadOnnx(Array.Empty())); + + /// + /// **二重解放が落ちないこと。** mat_table と同じ規約である。 + /// + [Fact] + public void DisposingTwiceIsSafe() + { + // 読めない入力なので net は作れない。**代わりに、 + // 解放済みの handle を native がどう扱うかを直接見る。** + // (CvNet を作れないので、ここは NativeMethodsDnn を直接叩く) + Assert.Equal( + (int)CvStatus.InvalidHandle, + CvUnity.Interop.Dnn.NativeMethodsDnn.ocvu_dnn_net_release(0)); + } +} diff --git a/tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx b/tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx new file mode 100644 index 0000000..1baf31e Binary files /dev/null and b/tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx differ diff --git a/tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx.py b/tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx.py new file mode 100644 index 0000000..7062659 --- /dev/null +++ b/tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx.py @@ -0,0 +1,38 @@ +"""tiny.onnx を生成するスクリプト。 + +**tiny.onnx は外から取ってきたモデルではなく、このスクリプトが作った。** +外部のモデルは (1) ライセンスの確認が要り、(2) リポジトリに数 MB が入り、 +(3) 上流が消すと CI が壊れる。この 3 つを避けるため、`Identity` 1 ノードだけの +最小の ONNX を自分で作る。 + +`tiny.onnx` は binary なので中身が読めない。作り方をここに残していないと、 +次に誰かが opset を上げたくなったときに作り直せない —— それがこのファイルが +モデルの隣に置いてある理由である。 + +実行方法(リポジトリ root から): + + uv run --with onnx python tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx.py + +**ライセンスの扱い**: このモデルは自作の合成物(重みも構造も無い恒等写像) +であり、第三者の権利が存在しないので THIRD_PARTY_NOTICES.md には何も +足していない。ライセンス表記が要るのは「よそから持ってきたもの」に対して +であって、自分で作ったものにはそもそも記載する third party が無い。 +""" + +import onnx +from onnx import helper, TensorProto + +# 入力をそのまま返すだけのネットワーク。**推論の正しさは OpenCV の責任で、 +# ここが確かめるのは「境界を通ったか」である。** +node = helper.make_node('Identity', ['input'], ['output']) +graph = helper.make_graph( + [node], 'tiny', + [helper.make_tensor_value_info('input', TensorProto.FLOAT, [1, 3, 4, 4])], + [helper.make_tensor_value_info('output', TensorProto.FLOAT, [1, 3, 4, 4])]) +model = helper.make_model(graph, opset_imports=[helper.make_opsetid('', 13)]) +onnx.checker.check_model(model) + +import os +out_path = os.path.join(os.path.dirname(__file__), 'tiny.onnx') +onnx.save(model, out_path) +print('bytes:', len(model.SerializeToString()), '->', out_path) diff --git a/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs b/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs index 4d1cef6..40bc71a 100644 --- a/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs +++ b/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs @@ -26,12 +26,16 @@ /// `ProjectSettings.asset` の `scriptingDefineSymbols` に /// `Standalone: OCVU_PROFILE_DNN` を置いて `dev.ps1 test-unity-editmode` を /// 走らせ、`tests/UnityProject/Library/ScriptAssemblies/` に -/// `CvUnity.Interop.Dnn.dll` と `CvUnity.Tests.Shared.Dnn.dll` が -/// 現れることを見る(最後の実測は 2026-09-06。合成した -/// `profile: "dnn"` の spec から**生成した**ファイルで確かめた)。 +/// `CvUnity.Interop.Dnn.dll` / `CvUnity.Tests.Shared.Dnn.dll` / +/// `CvUnity.Dnn.dll` が現れることを見る(最後の実測は 2026-09-09、**本物の +/// `bindings/spec/dnn.json` から生成した公開 API を含む** —— この 3 つ目は +/// M7c Task 4 で足した。この実行では上の「切れている」方向の 3 件が +/// 前提を欠いて赤くなる ―― defines が既に `OCVU_PROFILE_DNN` を含むため +/// 意図どおりで、それ以外の全テストは緑のままだった。確認後は +/// `scriptingDefineSymbols` を空 `{}` へ戻す)。 /// /// **「切れている」だけを見る検査は、綴り間違いと区別が付かない。** -/// `defineConstraints` の値を打ち間違えても、この 4 件は全部緑になる —— +/// `defineConstraints` の値を打ち間違えても、この 5 件は全部緑になる —— /// その 1 点だけは /// が塞ぐ(asmdef が実際に綴っている値と、この class が使う定数を比べる)。 /// @@ -41,6 +45,14 @@ public class ProfileGatingTests private const string StandardAssembly = "CvUnity.Interop"; private const string DnnSharedTestAssembly = "CvUnity.Tests.Shared.Dnn"; + /// + /// 公開 API 層(Runtime/Dnn/、M7c Task 4)。 と + /// 同じ define で切ってある —— こちらだけを切り忘れると、 + /// Interop.Dnn が消えたときに参照先を失って利用者のプロジェクトが + /// コンパイルエラーになる。 + /// + private const string DnnPublicApiAssembly = "CvUnity.Dnn"; + /// /// **この define の綴りは、下の各テストと asmdef の両方が使う。** /// 片方だけ変わると `TheGatedAsmdefsSpellTheDefineTheseTestsCheckFor` が落ちる。 @@ -121,10 +133,34 @@ public void TheDnnSharedTestAssemblyIsAbsentWithoutItsDefine() "defineConstraints が効いていない"); } + /// + /// **dnn の公開 API 層も、define が無ければコンパイルされない。** + /// + /// Interop.Dnn だけを切っても足りない —— それを参照する + /// CvUnity.Dnn が残ると、参照先を失って**利用者のプロジェクトが + /// コンパイルエラーになる。** + /// + [Test] + public void TheDnnPublicApiAssemblyIsAlsoAbsentWithoutItsDefine() + { + var defines = UnityEditor.PlayerSettings.GetScriptingDefineSymbols( + UnityEditor.Build.NamedBuildTarget.Standalone); + + // このプロジェクトは既定で OCVU_PROFILE_DNN を立てていない。 + // **前提が崩れたら、この検査は何も見ていないので落とす。** + Assert.That(defines, Does.Not.Contain(DnnDefine), + "このテストは OCVU_PROFILE_DNN が立っていないことを前提にしている"); + + var names = CompilationPipeline.GetAssemblies(AssembliesType.Editor) + .Select(a => a.name).ToList(); + Assert.That(names, Does.Not.Contain(DnnPublicApiAssembly), + "define が無いのに dnn の公開 API assembly がコンパイルされている"); + } + /// /// **綴りを見る。** /// - /// 上の 3 件はどれも「define が立っていない状態で assembly が現れない」を + /// 上の 4 件はどれも「define が立っていない状態で assembly が現れない」を /// 見ている。**`defineConstraints` の値を打ち間違えても、それは全部真である** /// —— 存在しない define は決して立たないので、その assembly は /// **どんな define を立てても永久に現れない。** 「切ってある」と @@ -149,7 +185,7 @@ public void TheGatedAsmdefsSpellTheDefineTheseTestsCheckFor() // 下の foreach は 1 度も回らずに緑になる。 Assert.IsNotEmpty(all, "asmdef が 1 つも拾えていない。走査が壊れている"); - foreach (var wanted in new[] { DnnAssembly, DnnSharedTestAssembly }) + foreach (var wanted in new[] { DnnAssembly, DnnSharedTestAssembly, DnnPublicApiAssembly }) { var matches = all.Where(a => a.name == wanted).ToList(); Assert.AreEqual(1, matches.Count, diff --git a/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs b/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs new file mode 100644 index 0000000..9e525c1 --- /dev/null +++ b/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs @@ -0,0 +1,46 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine.TestTools; + +/// +/// dnn profile の到達性を Player (IL2CPP) から確かめる、手で書いた唯一の入口。 +/// +/// **レビュー指摘(I1)。** 生成物 AbiReachabilityChecksDnn.CallEveryEntryPoint() +/// (tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs)は +/// spec が載せる dnn の 4 宣言を 1 回ずつ呼ぶために生成されているが、 +/// **どこからも呼ばれていなかった。** 標準 profile の同じ仕組みは +/// AbiSurfaceTests.cs / AbiSurfacePlayerTests.cs / WebSmokeRunner.cs +/// の 3 箇所から呼ばれているのに対し、dnn 側は 0 箇所だった。 +/// +/// **呼ばれない宣言そのものが strip の対象になる**ので、この穴は +/// 「検査が無い」以上に悪い —— 検査を足しても、消える宣言が消えたことに +/// 気づけない。M4 が手書きの 19 本のうち 7 本で実際に踏んだのと +/// 同じ形の欠落が、dnn の 4 本にそのまま開いていた。 +/// +/// **`CvUnity.Tests.PlayMode` 自体は `OCVU_PROFILE_DNN` で切られていない** +/// (dnn を使わない利用者の Player を壊さないため)。したがって +/// `CvUnity.Tests.Shared.Dnn` への参照は asmdef に足すが、使う側は +/// この `#if` で自分を守る —— define が立っていなければ、このクラスは +/// テストを 1 つも持たない空のクラスになる。 +/// +/// **CI はどのレーンも `OCVU_PROFILE_DNN` を立てない**ので、このテストは +/// 自動実行されない。`ProfileGatingTests` が確立した「正の方向は人が手で +/// 確かめる」という規約と同じ形である。手動での実測手順と結果は +/// `docs/roadmap.md` の `### M7c の判定`「穴を隠さず書く」にある。 +/// +public class AbiSurfaceDnnPlayerTests +{ +#if OCVU_PROFILE_DNN + [UnityTest] + public IEnumerator EveryDnnEntryPointIsReachable() + { + var called = AbiReachabilityChecksDnn.CallEveryEntryPoint(); + // **0 件で緑にしない。** spec が空でも「呼び終えた」と言えてしまう。 + // dnn の spec は 4 entry なので、ちょうど 4 であることまで見る + // (標準側の Assert.Greater(called, 10) より厳しくできるのは、 + // dnn の本数が固定で小さいため)。 + Assert.AreEqual(4, called, "dnn の宣言は 4 本のはず"); + yield return null; + } +#endif +} diff --git a/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs.meta b/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs.meta new file mode 100644 index 0000000..c793a00 --- /dev/null +++ b/tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 432bc0c2eaa54580a7cf6333a638e801 diff --git a/tests/UnityProject/Assets/Tests/PlayMode/CvUnity.Tests.PlayMode.asmdef b/tests/UnityProject/Assets/Tests/PlayMode/CvUnity.Tests.PlayMode.asmdef index 53b7e85..83c99d4 100644 --- a/tests/UnityProject/Assets/Tests/PlayMode/CvUnity.Tests.PlayMode.asmdef +++ b/tests/UnityProject/Assets/Tests/PlayMode/CvUnity.Tests.PlayMode.asmdef @@ -6,7 +6,8 @@ "CvUnity.Core", "CvUnity.Interop", "CvUnity.UnityIntegration", - "CvUnity.Tests.Shared" + "CvUnity.Tests.Shared", + "CvUnity.Tests.Shared.Dnn" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs b/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs new file mode 100644 index 0000000..fad3575 --- /dev/null +++ b/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs @@ -0,0 +1,32 @@ +// このファイルは生成物である。手で編集しないこと。 +// 正本: bindings/spec/*.json +// 生成: ./tools/dev.ps1 generate +// +// **なぜ在るか。** IL2CPP の stripping は、呼ばれない P/Invoke 宣言を +// 消せる。M4 の点検では、手書きの 19 本のうち 7 本が Editor でも +// Player でも一度も呼ばれていなかった。**呼ばれない宣言は、消えても +// 誰も気づかない。** ここは spec が載せる宣言を 1 つ残らず 1 回ずつ +// 呼ぶ。結果は見ない —— 呼べたことだけを見る。 +// +// 呼ばないのは spec が reachable: false と書いた関数だけで、 +// 理由は spec の reachableNote にある(印だけ付けて理由が +// 無い spec は SpecModel が拒む)。 + +using CvUnity.Interop.Dnn; + +public static class AbiReachabilityChecksDnn +{ + /// + /// 呼んだ宣言の本数を返す。C の entry point 1 本に対して C# の宣言が + /// 2 つある場合(byte[] 版とポインタ版)は 2 本と数える —— 消えるのは + /// entry point ではなく宣言のほうだからである。 + /// + public static int CallEveryEntryPoint() + { + NativeMethodsDnn.ocvu_dnn_net_read_onnx(null, 0, out _); + NativeMethodsDnn.ocvu_dnn_net_release(0UL); + NativeMethodsDnn.ocvu_dnn_blob_from_image(0UL, 0UL, 0.0, 0, 0, 0.0, 0.0, 0.0, 0, 0); + NativeMethodsDnn.ocvu_dnn_net_forward(0UL, 0UL, 0UL); + return 4; + } +} diff --git a/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs.meta b/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs.meta new file mode 100644 index 0000000..51fe854 --- /dev/null +++ b/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c1831d2604974e569ba2955b72dee582 \ No newline at end of file diff --git a/tests/UnityProject/Assets/Tests/Shared.Dnn/README.md b/tests/UnityProject/Assets/Tests/Shared.Dnn/README.md index 71647e1..9f36092 100644 --- a/tests/UnityProject/Assets/Tests/Shared.Dnn/README.md +++ b/tests/UnityProject/Assets/Tests/Shared.Dnn/README.md @@ -1,10 +1,22 @@ # CvUnity.Tests.Shared.Dnn -**dnn profile の到達性テストが入る場所。** いまは空である。 +**dnn profile の到達性テストが入る場所。** `bindings/spec/dnn.json` が +`"profile": "dnn"` を宣言したので、生成器(`./tools/dev.ps1 generate`)が +`AbiReachabilityChecks.Dnn.g.cs`(`AbiReachabilityChecksDnn.CallEveryEntryPoint()`。 +dnn の 4 entry を 1 回ずつ呼ぶ)をこの assembly の下に書き出している—— +**もう空ではない。** -`bindings/spec/dnn.json` に `"profile": "dnn"` を書いて `./tools/dev.ps1 generate` -を実行すると、生成器がこの assembly の下に `AbiReachabilityChecks.g.cs`(の profile -別名)を書き出す(D8)。**手で書かない。** +**ただし、生成された `CallEveryEntryPoint()` を実際に呼ぶ入口は、この +assembly の外にある**(`Shared` / `Shared.Dnn` はどちらも NUnit を参照しない +プレーンな C# ライブラリで、`[Test]` / `[UnityTest]` はここには置けない)。 +呼び出しは `tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs` +(`#if OCVU_PROFILE_DNN` で自分を守る)が担う。**この 1 箇所が無いと、 +IL2CPP の stripping が dnn の 4 宣言を消しても誰も気づけない**——レビューで +指摘され、追加した(詳細は同ファイルの docstring と `docs/roadmap.md` の +`### M7c の判定`)。 + +生成の仕組み自体は spec を書き換えて `./tools/dev.ps1 generate` を再実行すれば +自動で追従する(D8)。**手で書かない。** `OCVU_PROFILE_DNN` が立っていなければ、この assembly はコンパイルされない (`defineConstraints`)。**dnn の到達性テストが既定ビルドを壊さない**のは diff --git a/tools/opencv-config.psd1 b/tools/opencv-config.psd1 index 44c3982..32eb710 100644 --- a/tools/opencv-config.psd1 +++ b/tools/opencv-config.psd1 @@ -9,7 +9,7 @@ # calib3d -> calib / geometry / stereo / ptcloud # BUILD_LIST は依存を自動解決するので、実際にビルドされる集合は # これより大きくなり得る。実測値は build-manifest.json に記録する。 - Modules = @('core', 'imgproc', 'imgcodecs', 'objdetect', 'features', 'calib') + Modules = @('core', 'imgproc', 'imgcodecs', 'objdetect', 'features', 'calib', 'dnn') # platform ごとの toolchain。実行中の platform に対応する 1 つが選ばれ、 # 構成ハッシュに混ざる(Get-OpenCvConfigHash は Config 全体を正規化する)。 @@ -126,6 +126,52 @@ # 配布先の下限を固定する。指定しないとビルドマシンの OS 版に # 引きずられ、同じ構成ハッシュで別物ができる。 '-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0' + <# + **libpng の ARM NEON 最適化を切る(M7c、dnn の arm64 リンク失敗の調査で発見)。** + + `dnn` を Modules に足すと、OpenCV は 3rdparty/mlas を + `add_subdirectory` する。mlas は「plain ASM 言語が使えるか」を + `_MLAS_REQUIRES_ASM`(x86_64 / x86 / arm64 / loongarch64 で真)で + 判定し、使えなければ自分から `return()` して大人しく無効化する + (x86_64 の Windows / Linux は実際にこの経路を通り、 + `DNN MLAS: NO (no ASM compiler available for ...)` になる)。 + + 共通の `CMakeArgs` にある `-DCMAKE_ASM_COMPILER=NOTFOUND` は + 「plain ASM を使えなくする」ための唯一のこちら側の入力 + (mlas 自身の `check_language(ASM)` はこの pin を尊重する no-op + になる)。**ところが arm64 の 3 platform(Android / macOS / iOS) + だけ、mlas の判定に届く前に別の 3rdparty が先に plain ASM を + 本物にしてしまい、この pin が意味を持たなくなる。** + + その 3rdparty は **libpng** である + (`3rdparty/libpng/CMakeLists.txt` の `PNG_ARM_NEON` 分岐、 + 既定値は arm64 で `"on"`): `NOT PNG_ARM_NEON STREQUAL "off"` の + とき `enable_language(ASM)` を呼び、`arm/filter_neon.S` を + 足す。libjpeg-turbo(先に疑ったが、ログの並びを読み直すと + `add_subdirectory(simd)` は `SIMD extensions:` の行より前に + 完了しており的外れだった)や carotene / kleidicv + (在るが `enable_language` の呼び出しが見当たらない)ではなく、 + こちらが実測で確認できた唯一の候補である。 + + `-DPNG_ARM_NEON=off` にすると、この `enable_language(ASM)` が + 呼ばれなくなり(`arm/arm_init.c` 等の intrinsics 実装も含めて + NEON 最適化そのものが丸ごと無効になる)、mlas の判定が + x86_64 と同じ「plain ASM が無い」経路を取れるようになる。 + **これは仮説ではなく実測で確認済みである** —— run + `34309760354` で 3 platform とも `DNN MLAS: NO (no ASM + compiler available)` になり、`Building ASM object` / + `ASM compiler identification` がどちらも 0 件になった。 + **代償は明確: PNG の NEON デコード高速化を、Android / macOS / + iOS の 3 platform で失う。** + OpenCV 全体の `WITH_SIMD`(core/imgproc)や libjpeg-turbo の + JPEG SIMD には触れない——PNG だけに絞った変更である。 + + **x64 には要らない**(そもそも `-DCMAKE_ASM_COMPILER=NOTFOUND` + が効いており、mlas は既に無効)。web-wasm にも要らない + (dnn の `add_subdirectory(mlas)` 自体が `EMSCRIPTEN` で + skip される)。arm64 の 3 platform にだけ置く。 + #> + '-DPNG_ARM_NEON=off' ) 'linux-x64' = @( # 共有ライブラリへ静的ライブラリを取り込むため。 @@ -155,6 +201,11 @@ '-DANDROID_STL=c++_static' # 共有ライブラリへ静的ライブラリを取り込むため(linux と同じ)。 '-DCMAKE_POSITION_INDEPENDENT_CODE=ON' + # libpng の ARM NEON を切る。理由は macos-arm64 のコメントを参照 + # ——dnn/mlas が要求する「plain ASM が無い」状態を、libpng の + # enable_language(ASM) が arm64 で壊してしまうため。代償は PNG の + # NEON デコード高速化を失うこと。 + '-DPNG_ARM_NEON=off' <# **16 KB page size の flag はここに書かない。** @@ -178,6 +229,11 @@ # bitcode は Xcode 14 で廃止された。明示的に切らないと古い CMake が # 有効化しようとする。 '-DCMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE=NO' + # libpng の ARM NEON を切る。理由は macos-arm64 のコメントを参照 + # ——dnn/mlas が要求する「plain ASM が無い」状態を、libpng の + # enable_language(ASM) が arm64 で壊してしまうため。代償は PNG の + # NEON デコード高速化を失うこと。 + '-DPNG_ARM_NEON=off' ) <# @@ -377,7 +433,26 @@ # 与えないものを「気づかず有効」のままにしない(計画書 §8.2)。 '-DWITH_ITT=OFF' '-DBUILD_ITT=OFF' - '-DWITH_PROTOBUF=OFF' + + # protobuf は 2026-09-08 まで OFF だった —— dnn が Modules に無かったので、 + # このブロックの他の項目と同じ判断で「この project に何ら価値を与えない」 + # optional 依存として切っていた。**dnn を足した時点でその前提が失効した**: + # ONNX モデルは protobuf でシリアライズされているので、dnn にとって + # protobuf はもう optional ではない。消さずにここへ書き換えるのは、 + # 同じ誤解が別の場所(tools/verify-opencv-artifact.ps1 の denylist)にも + # 在ることを次に読む人が確かめられるようにするためである。 + # + # OFF のままだと OpenCV は OPENCV_DNN_EXTERNAL_PROTOBUF=1 のフォールバック + # 経路へ落ち、protoc が生成するはずの opencv-onnx.pb.h が無いままコンパイル + # しようとして落ちる(実測、run 34211332431、6 platform 全滅、約 4 分で失敗): + # fatal error: opencv-onnx.pb.h: No such file or directory + # + # BUILD_PROTOBUF=ON も明示する。上の BUILD_ZLIB / BUILD_PNG / BUILD_JPEG と + # 同じ理由 —— システムの protobuf に依存すると、ビルドしたランナーに何が + # 入っているかで成果物が変わり、再現性が崩れる。バンドル版を明示的に選ぶ。 + '-DWITH_PROTOBUF=ON' + '-DBUILD_PROTOBUF=ON' + '-DWITH_EIGEN=OFF' '-DWITH_OPENCL=OFF' '-DWITH_CUDA=OFF' diff --git a/tools/pack-upm-tarball.ps1 b/tools/pack-upm-tarball.ps1 index 577cb19..4fd30a7 100644 --- a/tools/pack-upm-tarball.ps1 +++ b/tools/pack-upm-tarball.ps1 @@ -39,10 +39,13 @@ param( # 出来上がった tarball の上限バイト数。既定は OpenUPM の 512 MB。 # # **引数にしてあるのは、落ちるところを見られるようにするためである。** - # 現状の配布物は上限に対して 1 桁以上小さいので、既定値のままでは + # 現状の配布物は上限よりまだ十分小さいので、既定値のままでは # この検査が働くところを誰も確かめられない(prove-a-check-works)。 # **具体的な倍率をここに書かない** —— 配布物が大きくなるたびに嘘になり、 - # 実際に一度そうなった(8.4 MB のとき 63 倍、9.6 MB で 55 倍)。 + # 実際に何度もそうなった(8.4 MB のとき 63 倍、9.6 MB で 55 倍、 + # M7c で dnn を足した後は 77.5 MB で 7 倍弱 —— 「1 桁以上」も + # 2026-09-10 の時点で既に嘘になっていたので、この行自体を数字の + # 要らない言い方に直した)。 [long] $MaxBytes = 536870912 ) diff --git a/tools/tests/VerifyOpenCvArtifact.Tests.ps1 b/tools/tests/VerifyOpenCvArtifact.Tests.ps1 index 48eb5fc..5a396b5 100644 --- a/tools/tests/VerifyOpenCvArtifact.Tests.ps1 +++ b/tools/tests/VerifyOpenCvArtifact.Tests.ps1 @@ -78,10 +78,21 @@ Assert-That ($LASTEXITCODE -eq 0) 'an accepted transitive module (flann) is perm # 非ゼロになるので、意図した拒否とクラッシュが区別できない(同じ理由を # `$undocumented` の検査も書いている。**行番号ではなく内容で引く** —— # 行番号は書いた瞬間から古くなる)。**拒んだ相手の名前まで見る。** -$withUnlisted = New-Tree ($allowed + @('opencv_dnn500.lib')) +# +# **ここは以前 opencv_dnn500.lib を「許可リストに無い module」の見本にしていたが、 +# `dnn` が Modules に足された 2026-09-08 にその前提が崩れ、このアサーションは +# 実際に赤くなった**(緑のまま意味を失ったのではない——`$allowed` は +# `$config.Modules` から動的に作るので、`opencv_dnn500.lib` は既に `$allowed` に +# 含まれるようになり、`$withUnlisted` は事実上ただの `$allowed` と同じ内容になった。 +# 検証スクリプトはこれを合格と判定して exit 0 を返し、 +# `Assert-That ($LASTEXITCODE -ne 0) ...` が FAIL した)。gapi はこのリポジトリの +# どの計画にも予定が無く、$config.Modules にも $AcceptedTransitiveModules にも +# 現れない —— それでも将来 gapi を足す日が来れば、この見本も同じ形で +# (今回と同じく、実際に赤くなって)古くなる。 +$withUnlisted = New-Tree ($allowed + @('opencv_gapi500.lib')) $unlistedOutput = & pwsh -NoProfile -File $verify -Root $withUnlisted 2>&1 | Out-String Assert-That ($LASTEXITCODE -ne 0) 'an OpenCV module outside the permitted set is rejected' -Assert-That ($unlistedOutput -match [regex]::Escape("OpenCV module 'dnn'")) ` +Assert-That ($unlistedOutput -match [regex]::Escape("OpenCV module 'gapi'")) ` 'the rejection names the module it refused (生の例外で落ちるのと区別が付く形で落ちること)' diff --git a/tools/verify-opencv-artifact.ps1 b/tools/verify-opencv-artifact.ps1 index dfb43ca..aa1cbf2 100644 --- a/tools/verify-opencv-artifact.ps1 +++ b/tools/verify-opencv-artifact.ps1 @@ -150,6 +150,29 @@ $PermittedThirdPartyLibs = @( # **binary 形式での再頒布に著作権表示と免責の同梱を求める**ので、 # THIRD_PARTY_NOTICES.md に全文を入れてある。Apache-2.0 と両立する。 'cpufeatures' + + # **dnn を足したので protobuf が入る。** + # + # 以前はここに denylist の項があり、「protobuf は dnn 用で allowlist 外」と + # 書いてあった。**dnn を足した時点でその前提が失効した** —— ONNX モデルは + # protobuf でシリアライズされているので、dnn にとって protobuf はもう + # optional でも allowlist 外でもない。消さずにこう書き換えるのは、 + # 同じ誤解が別の場所にも在るかを次に読む人が確かめられるようにするためである。 + # + # **名前は 'protobuf' ではなく 'libprotobuf'。** 上流の protobuf プロジェクト + # 自身が CMake target を 'libprotobuf' と名付けているため、OpenCV が + # そのままバンドルすると Windows は libprotobuf.lib、Unix は 'lib' 接頭辞が + # 二重に付いた liblibprotobuf.a になる(実測: run 34212296352、6 platform + # 全部で確認。Windows は libprotobuf.lib、他 5 platform は liblibprotobuf.a)。 + # 判定側は拡張子と 'lib' 接頭辞を 1 回だけ剥がすので、'libprotobuf' の 1 エントリで + # 両方(stem 一致 / stripped 一致)を拾える。'protobuf' だけでは Unix 側の + # liblibprotobuf.a(stripped => 'libprotobuf')を拾えない。 + # + # **ライセンスは一次情報で確認: BSD-3-Clause** + # (https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)。 + # THIRD_PARTY_NOTICES.md に全文を足すまで、この行だけで緑にしない —— + # 利用者が読む文書は何も赤くならない(add-a-platform skill の罠 5)。 + 'libprotobuf' ) # 名前に現れたら拒否理由を具体的に説明できるもの。 @@ -174,7 +197,10 @@ $knownBadPatterns = @( # 無関係なファイル名の途中にたまたま ipp を含むものまで拾ってしまう。 # OpenCV 自身の module 名は opencv_ で始まり ipp を名乗らないので衝突しない。 @{ Pattern = 'ipp*'; Why = 'IPP は Intel の独自条項。有効化は M7 で検討する' } - @{ Pattern = '*protobuf*'; Why = 'protobuf は dnn 用で allowlist 外' } + # protobuf はここに在った(*protobuf*、「protobuf は dnn 用で allowlist 外」)。 + # **dnn を Modules に足した時点でその前提が失効した** —— dnn にとって + # protobuf はもう「allowlist 外にしてよい依存」ではない。$PermittedThirdPartyLibs + # 側に移した理由はそちらのコメントに書いてある。 @{ Pattern = '*libtiff*'; Why = 'TIFF は allowlist 外' } @{ Pattern = '*libwebp*'; Why = 'WebP は allowlist 外' } @{ Pattern = '*openexr*'; Why = 'OpenEXR は allowlist 外' } @@ -252,6 +278,11 @@ $InertLicenseFiles = @( # Android のビルドにだけ現れる(NDK の cpufeatures)。 'cpufeatures-LICENSE' 'cpufeatures-README.md' + + # dnn を足したので現れる(実測: run 34212296352、6 platform 全部)。 + # $PermittedThirdPartyLibs の libprotobuf の項を見よ。 + 'protobuf-LICENSE' + 'protobuf-README.md' ) # Valgrind の抑制ファイル。Unix 系の install だけが置く(Windows のビルドには # 現れない — 実測で確認)。実行可能コードではなく、Valgrind に「この警告は