From 9eb7356aa877e99779f07757fec0a62e64eb1f30 Mon Sep 17 00:00:00 2001 From: yousan Date: Tue, 8 Sep 2026 18:33:53 +0900 Subject: [PATCH 01/21] =?UTF-8?q?docs(roadmap):=20M7=20=E3=81=AE=20DNN=20g?= =?UTF-8?q?ate=20lift=20=E3=81=A8=E3=83=8D=E3=82=A4=E3=83=86=E3=82=A3?= =?UTF-8?q?=E3=83=96=20binary=20=E5=8D=98=E4=B8=80=E5=8C=96=E3=81=AE?= =?UTF-8?q?=E6=B1=BA=E5=AE=9A=E3=82=92=E8=A8=98=E9=8C=B2=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「5.0 に固定した DNN ラッパーを作り込まない」は 2026-09-08、リポジトリ所有者の 依頼により解除した。module 分離(M7b、2026-09-06)は済んでおり、上流の作り直し リスクを提示したうえで所有者が着手を依頼した。5.1 の根拠自体は否定されていない。 あわせて、dnn の配布の形について決まったこと(native binary は profile ごとに 分けない。iOS / WebGL の __Internal 静的リンクには切り替える対象が無いため)と、 まだ決めていないこと(全部入り tarball を 1 つの artifact に保つか)を記録した。 --- docs/roadmap.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 4440186..665a413 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2106,6 +2106,22 @@ 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 にリポジトリ所有者から「M7 の残作業を進めてほしい」という +明示的な依頼があったことで満たした —— **このとき、上で挙げた 5.1 での作り直し +リスクをリポジトリ所有者に提示し、それを承知のうえでの依頼である。** + +**元の決定を支えていた根拠は、これによって否定されてはいない。** 旧エンジンの +削除、`enum EngineType` の再番号、`OPENCV_FORCE_DNN_ENGINE` の意味変化という上流の +実測はいまも成立したままで、**上流の安定を示す新しい証拠は出ていない。** 変わった +のはただ 1 点 —— **リポジトリ所有者が、その作り直しの費用を承知のうえで受け入れる +と判断したこと**である。**したがって、5.0 の DNN API に対して作るコードは、5.1 が +来たときに作り直しが要ると見込んでよい。** 後でこれに驚く読み手が出るなら、この文を +ここに見つけられるようにしてある。 + **まだ決めていないこと**(M5 / M7 で決める) - **`OCVU_ABI_VERSION` は単一の整数のまま**である(正本は @@ -2113,9 +2129,43 @@ M3.5 節を参照)、`ocvu_imencode` / `ocvu_imdecode` を出した。ここ module 分離がこの決定に影響しうるなら、**正本のほうに留保を書く** - dnn を**別 package**(`…-dnn`)で配るか、同じ package の optional profile にするか。 **全部入り tarball を配る正にしたのは M3.5 の決着**なので、dnn を足すことは - **「中身を足す」ではなく「形を変える」**ことになりうる + **「中身を足す」ではなく「形を変える」**ことになりうる。**2026-09-08 に、隣接する + 問い(native binary を profile ごとに分けるか)だけ決まった —— 分けない、と決めた。 + この問いそのものはまだ決めていない。** 詳細と選択肢は下の「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 を一度も有効にしない利用者にも配られる +ことになる。 + +**まだ決めていない: 全部入り tarball が 1 つの artifact のままかどうか。** 誰も +測っていない数 —— `dnn` と protobuf を積んだときの platform あたりの増分 —— +に懸かっている。全部入りは現在 **69,565,901 バイト(66 MB。6 platform、v0.3.0 の +公開済み asset、2026-09-06 実測 —— 上の「決定: native bridge を module 単位に +分ける」5 に同じ実測がある)**で、`.github/workflows/release.yml` は必須チェックの +中で **100 MB** の上限を課しており、残りはおよそ **34 MB**(6 platform で割ると +1 platform あたり約 5.6 MB。`docs/superpowers/plans/2026-09-05-m7c-dnn-profile.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 つ目の箇条) + **差別化としての位置づけは変えない。** 競合が書き直し前のエンジンを載せている点は [競合調査](./unity-opencv-integration-research-and-plan.md) §3 / §4.6 のとおりで、 **Unity 利用者には推論エンジンの代替がある**という理由も変わらない。**変わったのは @@ -2123,7 +2173,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) From b5c252a1181b36a9115ee5d92f7a6ecc72c42237 Mon Sep 17 00:00:00 2001 From: yousan Date: Tue, 8 Sep 2026 18:40:47 +0900 Subject: [PATCH 02/21] =?UTF-8?q?build(m7c):=20OpenCV=20=E3=81=AE=20Module?= =?UTF-8?q?s=20=E3=81=AB=20dnn=20=E3=82=92=E8=B6=B3=E3=81=99=EF=BC=88?= =?UTF-8?q?=E6=A7=8B=E6=88=90=E3=83=8F=E3=83=83=E3=82=B7=E3=83=A5=E3=81=8C?= =?UTF-8?q?=E5=A4=89=E3=82=8F=E3=82=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/opencv-config.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/opencv-config.psd1 b/tools/opencv-config.psd1 index 44c3982..a04c5bb 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 全体を正規化する)。 From 208c1ce32e8a26956cfee8ce8b0be8219c8c0769 Mon Sep 17 00:00:00 2001 From: yousan Date: Tue, 8 Sep 2026 18:46:21 +0900 Subject: [PATCH 03/21] =?UTF-8?q?docs(roadmap):=20DNN=20gate=20lift=20?= =?UTF-8?q?=E3=81=AE=E6=A0=B9=E6=8B=A0=E3=82=92=E3=80=81=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E3=81=A8=20provenance=20=E3=81=AE=E6=98=8E=E8=A8=98=E3=81=A7?= =?UTF-8?q?=E8=A3=9C=E5=BC=B7=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit レビューの Important 指摘を受け、所有者の依頼を要約ではなく原文のまま 引用する(何を決めるべきか尋ねた発言と、作り直しリスクを提示された うえでの「進めてください」の2つ、順序込みで)。あわせて、この経緯の 記録がこの commit にしか無く、issue・ticket・外部記録が無いことを 文書自身の声で明記する。 --- docs/roadmap.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 665a413..1dfd724 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2110,9 +2110,27 @@ M3.5 節を参照)、`ocvu_imencode` / `ocvu_imdecode` を出した。ここ `docs/superpowers/plans/2026-09-05-m7c-dnn-profile.md` が着手の前提として掲げていた 2 つ —— (1) module 分離が済んでいること、(2) 利用者の要望か上流の安定を示す新しい 事実が出ていること —— のうち、(1) は `### M7b の判定` が 2026-09-06 に満たした。 -(2) は、2026-09-08 にリポジトリ所有者から「M7 の残作業を進めてほしい」という -明示的な依頼があったことで満たした —— **このとき、上で挙げた 5.1 での作り直し -リスクをリポジトリ所有者に提示し、それを承知のうえでの依頼である。** + +(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` の意味変化という上流の From 95aca49bfc0a5006e029282db5bb95e713b85f81 Mon Sep 17 00:00:00 2001 From: yousan Date: Tue, 8 Sep 2026 18:51:11 +0900 Subject: [PATCH 04/21] =?UTF-8?q?fix(m7c):=20dnn=20=E3=81=AB=E3=81=AF=20pr?= =?UTF-8?q?otobuf=20=E3=81=8C=E8=A6=81=E3=82=8B=E3=80=82WITH=5FPROTOBUF=20?= =?UTF-8?q?=E3=82=92=20ON=20=E3=81=AB=E3=81=97=20bundled=20=E7=89=88?= =?UTF-8?q?=E3=82=92=E6=98=8E=E7=A4=BA=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **1 回目の build-opencv.yml(run 34211332431)が 6 platform 全滅した。** WITH_PROTOBUF=OFF のまま dnn を足したため、OpenCV が OPENCV_DNN_EXTERNAL_PROTOBUF=1 のフォールバック経路へ落ち、protoc が 生成するはずの opencv-onnx.pb.h が無いままコンパイルしようとして fatal error で落ちていた(約 4 分で失敗、ビルドの失敗であって 依存 allowlist の判定にはまだ届いていない)。 protobuf を『価値を与えない optional 依存』として切っていた判断は、 dnn を足した時点で前提が崩れている —— ONNX の読み込みに要るので、 dnn にとって protobuf はもう optional ではない。消さずに理由を 書き換えたのは、同じ誤解が tools/verify-opencv-artifact.ps1 の denylist にも残っていることを次の読者が確かめられるようにするため。 BUILD_PROTOBUF=ON も明示し、システムの protobuf ではなくバンドル版を 選ぶ(BUILD_ZLIB/PNG/JPEG=ON と同じ、再現性のための判断)。 --- tools/opencv-config.psd1 | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tools/opencv-config.psd1 b/tools/opencv-config.psd1 index a04c5bb..d11a8e2 100644 --- a/tools/opencv-config.psd1 +++ b/tools/opencv-config.psd1 @@ -377,7 +377,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' From bceb7fc23eda9287f0fd06ead18e3d0c83bb7758 Mon Sep 17 00:00:00 2001 From: yousan Date: Tue, 8 Sep 2026 19:25:14 +0900 Subject: [PATCH 05/21] =?UTF-8?q?build(m7c):=20dnn=20=E3=81=8C=E9=80=A3?= =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=8F=E3=82=8B=20protobuf=20=E3=82=92=20a?= =?UTF-8?q?llowlist=20=E3=81=A8=20notices=20=E3=81=AB=E5=85=A5=E3=82=8C?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **依存 allowlist が意図どおり働いた。** run 34212296352(WITH_PROTOBUF=ON に直した後の 2 回目のビルド)は OpenCV 自体のコンパイルは 6 platform 全部 通ったが、verify dependency allowlist が libprotobuf.lib / liblibprotobuf.a と、その license ファイル 2 つ(拡張子 '' の protobuf-LICENSE、拡張子 '.md' の protobuf-README.md)を allowlist 外 として落とした。 denylist の *protobuf* 項は消さず、$PermittedThirdPartyLibs 側に 'libprotobuf' というエントリで書き換えた —— 同じ誤解が別の場所にも 在るかを次に読む人が確かめられるようにするため。名前が 'protobuf' では なく 'libprotobuf' なのは、上流 protobuf 自身の CMake target 名がそう だからで(Windows: libprotobuf.lib、他 5 platform: liblibprotobuf.a、 'lib' 接頭辞が二重になる)、判定側の stem/stripped 照合をローカルで シミュレートして確認した。 THIRD_PARTY_NOTICES.md:843 が明文で指示していた作業のうち、protobuf の 新規追加分を実行した —— ライセンスは一次情報(OpenCV 自身が 5.0.0 tag で vendor している 3rdparty/protobuf/LICENSE、upstream protobuf v3.19.1 tag と byte-for-byte 一致)で確認し、BSD-3-Clause の全文を足した。 「Modules built for this configuration」の一覧にも dnn を足した。 **dlpack / flatbuffers の再検索はまだ実行していない** —— run 34212296352 は allowlist で止まったため artifact が公開されておらず、grep できる ツリーがまだ無い。「present but not linked」節は、前提(dnn/gapi が Modules に無い)が dnn の追加で半分崩れたことと、再検索がまだ済んで いないことの両方を明記する形に直した。ビルドが緑になり artifact を restore できた時点で、実際に検索して結果どおりに直す。 ついでに tools/tests/VerifyOpenCvArtifact.Tests.ps1 の 「許可リストに無い OpenCV module は拒まれる」検査が opencv_dnn500.lib を 見本にしていたことに気づいた —— dnn を Modules に足した時点でこの見本は 「許可リストに無い」ものではなくなり、検査が意味を失っていた($allowed は $config.Modules から動的に作るため、dnn は自動的に許可される側へ 移っていた)。見本を gapi に差し替えた。 --- THIRD_PARTY_NOTICES.md | 138 +++++++++++++++++---- tools/tests/VerifyOpenCvArtifact.Tests.ps1 | 11 +- tools/verify-opencv-artifact.ps1 | 33 ++++- 3 files changed, 156 insertions(+), 26 deletions(-) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 429e22e..9f37f9b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -23,8 +23,8 @@ 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 @@ -816,33 +816,125 @@ 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. +``` + +--- + ## 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. +The searches below were last run against a tree built **before** `dnn` was +in `Modules`. That premise changed on 2026-09-08 (see the `protobuf` entry +above), and this section is only half updated as a result — **read the note +after the list before trusting either bullet.** - **dlpack** (Apache License 2.0) — - `third_party/opencv// のライセンスディレクトリ(上表)dlpack-LICENSE`. Searched - for `DLManagedTensor`, `DLPackVersioned`, `dlpack` (case-insensitive) - across every `.lib`; zero matches. + `third_party/opencv// のライセンスディレクトリ(上表)dlpack-LICENSE`. Last + searched for `DLManagedTensor`, `DLPackVersioned`, `dlpack` + (case-insensitive) across every `.lib`; zero matches **at that time**. - **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. + Last searched for `flatbuffers::`, `FlatBufferBuilder`, `flatbuffers_`; + zero matches **at that time**. (`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.) + +**This document previously said both belonged to OpenCV modules (`dnn`, +`gapi`) that were not in this configuration's `Modules` list. That premise +is now half false: `dnn` was added to `Modules` on 2026-09-08.** `gapi` +still isn't, and still isn't linked by anything this package ships. + +**The re-run this section's own previous instruction called for has not +happened yet, and this document does not claim it has.** The build that +added `dnn` (run `34212296352`) failed at the dependency-allowlist step +before installing a downloadable artifact — see the `protobuf` entry above +— so there is not yet a tree to search. Compiler command lines captured +from that same failed build already show both being compiled into `dnn`'s +sources (`-DHAVE_FLATBUFFERS=1` on every platform, and an +`-I .../3rdparty/dlpack/include` include path on the very `cast2_layer.cpp` +translation unit that made the build fail). That is evidence code that +*references* them is being built, not evidence that their *symbols* survive +into a final `.lib` — dlpack in particular is header-only, so whether +`DLManagedTensor` shows up in any `.lib` depends on whether anything +actually instantiates it, which the compiler flags alone don't say. +**Do not treat this paragraph as the re-run — it reproduces no license text +for either component, and it was not produced by grepping an artifact.** +Once a build with `dnn` completes and its artifact can be restored +(`./tools/opencv.ps1 restore`), re-run the exact searches above against it +and either move each component up into a reproduced section with its full +license text, or replace "at that time" above with a current negative +result — whichever the search actually shows. --- diff --git a/tools/tests/VerifyOpenCvArtifact.Tests.ps1 b/tools/tests/VerifyOpenCvArtifact.Tests.ps1 index 48eb5fc..450046e 100644 --- a/tools/tests/VerifyOpenCvArtifact.Tests.ps1 +++ b/tools/tests/VerifyOpenCvArtifact.Tests.ps1 @@ -78,10 +78,17 @@ 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` から動的に作るので、`dnn` は +# もう「外にある」module ではない)。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 に「この警告は From 2068cb0514546e20c1a3471b261e3c40029242d8 Mon Sep 17 00:00:00 2001 From: yousan Date: Tue, 8 Sep 2026 20:10:34 +0900 Subject: [PATCH 06/21] =?UTF-8?q?docs(m7c):=20=E5=AE=9F=E7=89=A9=E3=81=AE?= =?UTF-8?q?=E3=83=84=E3=83=AA=E3=83=BC=E3=81=A7=20dlpack/flatbuffers=20?= =?UTF-8?q?=E3=82=92=E5=86=8D=E6=A4=9C=E7=B4=A2=E3=81=97=E3=80=81flatbuffe?= =?UTF-8?q?rs=20=E3=82=92=E7=A7=BB=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run 34215362804 が 6 platform 全部で緑になり、初めて dnn 入りの artifact が 公開された。restore して THIRD_PARTY_NOTICES.md:843 の指示どおり実際に 検索した結果、flatbuffers は dnn の TFLite importer(opencv_tflite 名前空間、 Table / VerifierTemplate / Vector / String / GetRoot 等の読み取り側だけ)に リンクされていることを確認した。「present but not linked」から「リンク済み」 節へ移し、OpenCV 自身が vendor する 5.0.0 tag の LICENSE.txt と byte-for-byte 一致する Apache-2.0 全文を足した。 **前回のコミットで「検索はまだ実行していない」と書いた文章は、検索方法自体に 欠陥があったことも合わせて分かった** —— flatbuffers:: のような '::' を含む パターンは MSVC の name mangling では一致しない('::' がそのまま残らず '@' に 置き換わるため)。素の識別子(flatbuffers、句読点なし)で検索し直すと opencv_dnn500.lib に大量の一致が見つかった。この訂正を flatbuffers の節に 明記し、今後の検索でも同じ罠を踏まないようにした。 dlpack は同じ修正済みの検索方法(DLTensor / DLDevice / DLDataType / DLManagedTensor / dlpack を句読点なしで)でも実物のツリーに対して 0 件で、 「present but not linked」に残した —— header-only な型定義だけのライブラリ なので、参照されても distinct な symbol を残さないことと整合する。 ついでに「Universe considered」の platform ごとの内訳を実測し直した。 protobuf の 2 ファイルが全 platform に増えたことに加え、dnn とは無関係な 既存の相違(macOS / iOS が clapack-lapack_LICENSE を持たない、iOS だけ dlpack-LICENSE も持たない)が、個別に数えて初めて見えた。 --- THIRD_PARTY_NOTICES.md | 352 +++++++++++++++++++++++++++++++++++------ 1 file changed, 308 insertions(+), 44 deletions(-) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 9f37f9b..a52d2ea 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -28,12 +28,26 @@ source, which is Apache-2.0 (see [LICENSE](LICENSE)). (`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 @@ -891,50 +905,300 @@ 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`, hash `c21fe00b8952`) and searching its +`opencv_dnn500.lib`, not by inference from compiler flags. + +**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 -The searches below were last run against a tree built **before** `dnn` was -in `Modules`. That premise changed on 2026-09-08 (see the `protobuf` entry -above), and this section is only half updated as a result — **read the note -after the list before trusting either bullet.** +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`. Last - searched for `DLManagedTensor`, `DLPackVersioned`, `dlpack` - (case-insensitive) across every `.lib`; zero matches **at that time**. -- **flatbuffers** (Apache License 2.0) — - `third_party/opencv// のライセンスディレクトリ(上表)flatbuffers-LICENSE.txt`. - Last searched for `flatbuffers::`, `FlatBufferBuilder`, `flatbuffers_`; - zero matches **at that time**. (`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.) - -**This document previously said both belonged to OpenCV modules (`dnn`, -`gapi`) that were not in this configuration's `Modules` list. That premise -is now half false: `dnn` was added to `Modules` on 2026-09-08.** `gapi` -still isn't, and still isn't linked by anything this package ships. - -**The re-run this section's own previous instruction called for has not -happened yet, and this document does not claim it has.** The build that -added `dnn` (run `34212296352`) failed at the dependency-allowlist step -before installing a downloadable artifact — see the `protobuf` entry above -— so there is not yet a tree to search. Compiler command lines captured -from that same failed build already show both being compiled into `dnn`'s -sources (`-DHAVE_FLATBUFFERS=1` on every platform, and an -`-I .../3rdparty/dlpack/include` include path on the very `cast2_layer.cpp` -translation unit that made the build fail). That is evidence code that -*references* them is being built, not evidence that their *symbols* survive -into a final `.lib` — dlpack in particular is header-only, so whether -`DLManagedTensor` shows up in any `.lib` depends on whether anything -actually instantiates it, which the compiler flags alone don't say. -**Do not treat this paragraph as the re-run — it reproduces no license text -for either component, and it was not produced by grepping an artifact.** -Once a build with `dnn` completes and its artifact can be restored -(`./tools/opencv.ps1 restore`), re-run the exact searches above against it -and either move each component up into a reproduced section with its full -license text, or replace "at that time" above with a current negative -result — whichever the search actually shows. + `third_party/opencv// のライセンスディレクトリ(上表)dlpack-LICENSE`. Searched + the restored `dnn`-enabled tree (run `34215362804`, hash `c21fe00b8952`) + 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. + +**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. --- From 19a3bcc915a34d2f6069e0457fdf8542a7aca4c1 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 01:26:53 +0900 Subject: [PATCH 07/21] =?UTF-8?q?fix(m7c):=20THIRD=5FPARTY=5FNOTICES.md=20?= =?UTF-8?q?=E3=81=8C=E6=A7=8B=E6=88=90=E3=83=8F=E3=83=83=E3=82=B7=E3=83=A5?= =?UTF-8?q?=E3=82=92=E7=9B=B4=E6=9B=B8=E3=81=8D=E3=81=97=E3=81=A6=E3=81=84?= =?UTF-8?q?=E3=81=9F=E3=81=AE=E3=82=92=E5=A4=96=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PackageRelease.Tests.ps1 の「the tracked notices do not hardcode a configuration hash」が実際に落ちて見つかった —— flatbuffers / dlpack の 節に c21fe00b8952 を 2 箇所書いてしまっていた。CLAUDE.md 自身が M3 で 19 箇所死んだ例として警告している形と同じ間違いである。run ID (34215362804)だけを残し、ハッシュは削った。 --- THIRD_PARTY_NOTICES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index a52d2ea..e98eaf6 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -925,8 +925,8 @@ serializes them. 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`, hash `c21fe00b8952`) and searching its -`opencv_dnn500.lib`, not by inference from compiler flags. +`./tools/opencv.ps1 restore`) and searching its `opencv_dnn500.lib`, not by +inference from compiler flags. **A methodology correction, recorded so the next search doesn't repeat it:** the two earlier "zero matches" results for this component used patterns @@ -1177,8 +1177,8 @@ section too. - **dlpack** (Apache License 2.0) — `third_party/opencv// のライセンスディレクトリ(上表)dlpack-LICENSE`. Searched - the restored `dnn`-enabled tree (run `34215362804`, hash `c21fe00b8952`) - for the bare identifiers `DLTensor`, `DLDevice`, `DLDataType`, + 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. From 86f1004ad954c19543017012e8503c24c07e26c3 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 11:10:07 +0900 Subject: [PATCH 08/21] =?UTF-8?q?feat(m7c):=20ocvu=5Fnet=5Fhandle=20?= =?UTF-8?q?=E3=81=AE=E8=A1=A8=E3=82=92=E3=80=812=20=E3=81=A4=E7=9B=AE?= =?UTF-8?q?=E3=81=A8=E3=81=97=E3=81=A6=E5=85=88=E3=81=AB=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M3 の PR #8 が mat_table で踏んだ形を、2 つ目の表で再生産しない。 Slot が値でオブジェクトを持つと、表が伸びたときに先に解決したポインタが ぶら下がる —— 壊れるのは伸ばした側ではなく無関係な handle を使っている側で、 2 つのスレッドがそれぞれ自分のオブジェクトだけを触るという正しい使い方で壊れる。 確率に頼らない。伸びを強制してから古いポインタを触る形にしたので、 落ちるときは決定的に落ちる(PR #8 のときはローカル 3 回と CI 3 回が緑で、 1 度だけ落ちた —— フレークとして再実行していたら残っていた)。 負の対照: Slot を値に戻すと該当 1 件が落ち、ASan では use-after-free が出る。 cmake/FindOpenCvUnityDeps.cmake の COMPONENTS に dnn を足した。 unique_ptr の破棄に cv::dnn::Net の定義が要るため、 これが無いと L1 がリンクできない。COMPONENTS に足すだけでは binary は 1 バイトも増えない(M3.5 / M5 で 2 度実測済みの性質)—— 増えるのは Task 3 が ocvu_ 関数を実装したときである。 modules.cmake の OCVU_MODULE_dnn は src/ocvu_dnn_table.cpp だけを持つ。 src/ocvu_dnn.cpp は Task 3 の成果物でまだ存在しないため、いま足すと configure が落ちる。 この時点で dev.ps1 test は 2 件だけ赤くなる(どちらも Task 3 が spec を 作るまで閉じない、想定内の赤): - OpenCvConfig.Tests.ps1: "every built module is exposed through the spec (spec に無いのに Modules に在る: dnn)" - BindingGenerator.Tests.ps1: "modules.cmake と bindings/spec の module 一覧が食い違う => dnn" L1 (test-native) は 219 件 全 PASS(215 → 219)、test-managed は 116 件 全 PASS で無影響。 --- cmake/FindOpenCvUnityDeps.cmake | 2 +- native/include/opencv_unity_native.h | 8 ++ native/modules.cmake | 7 +- native/src/ocvu_dnn_table.cpp | 103 ++++++++++++++++++++++ native/src/ocvu_dnn_table.h | 23 +++++ native/tests/CMakeLists.txt | 1 + native/tests/test_dnn_table_stability.cpp | 73 +++++++++++++++ 7 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 native/src/ocvu_dnn_table.cpp create mode 100644 native/src/ocvu_dnn_table.h create mode 100644 native/tests/test_dnn_table_stability.cpp 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/native/include/opencv_unity_native.h b/native/include/opencv_unity_native.h index f138846..98d2d86 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 の型。 * diff --git a/native/modules.cmake b/native/modules.cmake index 2e977ae..c912754 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) +# **dnn は Task 3 で ocvu_dnn.cpp(C ABI 関数本体)を足すまでこの 1 本だけ。** +# ここに存在しないファイルを書くと configure が落ちる —— handle 表だけを +# 先に固定するのがこの module の Task 2 の役目である。 +set(OCVU_MODULE_dnn src/ocvu_dnn_table.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_table.cpp b/native/src/ocvu_dnn_table.cpp new file mode 100644 index 0000000..c41be0b --- /dev/null +++ b/native/src/ocvu_dnn_table.cpp @@ -0,0 +1,103 @@ +#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(); +} + +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..250883d --- /dev/null +++ b/native/src/ocvu_dnn_table.h @@ -0,0 +1,23 @@ +#pragma once + +#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); + +} // namespace ocvu diff --git a/native/tests/CMakeLists.txt b/native/tests/CMakeLists.txt index 1db0e37..82feea0 100644 --- a/native/tests/CMakeLists.txt +++ b/native/tests/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable(ocvu_tests test_core_ops.cpp test_matching.cpp test_stereo.cpp + test_dnn_table_stability.cpp ) # SHARED ではなく STATIC 側にリンクする(native/CMakeLists.txt の注記を参照) diff --git a/native/tests/test_dnn_table_stability.cpp b/native/tests/test_dnn_table_stability.cpp new file mode 100644 index 0000000..2f0c87a --- /dev/null +++ b/native/tests/test_dnn_table_stability.cpp @@ -0,0 +1,73 @@ +// **2 つ目の handle 表が、1 つ目と同じ欠陥を持たないことを決定的に固定する。** +// +// M3 の PR #8 で mat_table が踏んだ形: Slot が値でオブジェクトを持ち、 +// get が配列内部のポインタを返すと、別スレッドの add で配列が伸びたときに +// **先に解決したポインタが全部ぶら下がる。** +// +// **ローカル 3 回と直前 3 回の CI が緑で、1 度だけ落ちた。** +// フレークとして再実行していたら残っていた。だからここでは +// **確率に頼らず、伸びを強制してから古いポインタを触る。** +#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); + + // 表を大きく伸ばす。**vector が値を持っていれば、ここで再配置が起きる。** + std::vector others; + for (int i = 0; i < 4096; ++i) { + others.push_back(ocvu::net_table_add(std::make_unique())); + } + + // **先に解決したポインタがまだ生きていること。** + 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); +} From eddb2196cdeb016b66b865428326c92aa9eb1344 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 11:21:06 +0900 Subject: [PATCH 09/21] =?UTF-8?q?docs(m7c):=20=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=81=AE=20Minor=202=20=E4=BB=B6=E3=82=92=E7=9B=B4?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor 1: flatbuffers/dlpack の再検索が Windows の artifact だけに基づいていた のに、通知は 6 platform 全部について無条件に「リンクされている/いない」と 主張していた。team-lead が run 34215362804 の Linux artifact (libopencv_dnn.a)を実際に検索し、flatbuffers 164 件・dlpack 0 件(tree 全体 でも 0 件)を確認した。**この 2 点目は単なる 2 台目ではない** —— Windows は MSVC の name mangling、Linux は Itanium の name mangling で、今回見つけた 検索手法の欠陥('::' を含むパターンが mangling 方式に依存する)がまさに この軸で変わるものだったため、2 つの mangling 方式の両方で分類が保たれた ことを示す方が、単に platform 数を増やすより意味が重い。残る 4 platform (macOS / iOS / Android / Web)は実際には検索しておらず、同じ Modules・ 同じ source tree からの推論であることを明記した——ここは正直に「未検査」の ままにする。 Minor 2: tools/tests/VerifyOpenCvArtifact.Tests.ps1:82-84 のコメントが 「dnn を Modules に足したことで、この見本が緑のまま意味を失った」と書いて いたが、実際に起きたのはその逆——`$allowed` が動的に dnn を含むようになった 結果 `$withUnlisted` が実質ただの `$allowed` になり、検証は合格 (exit 0) を 返して `Assert-That ($LASTEXITCODE -ne 0)` が FAIL した。**緑のまま腐ったの ではなく、赤くなった。** このリポジトリの語彙ではこの 2 つは正反対で (検査が静かに緑のまま無意味になることこそ最も恐れる壊れ方で、赤くなる のはその壊れ方が起きていないことの証拠である)、コメントを実際に起きた 方へ書き換えた。 --- THIRD_PARTY_NOTICES.md | 20 +++++++++++++++++++- tools/tests/VerifyOpenCvArtifact.Tests.ps1 | 14 +++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index e98eaf6..3c5a9db 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -928,6 +928,19 @@ 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 @@ -1181,7 +1194,12 @@ section too. 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. + `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 diff --git a/tools/tests/VerifyOpenCvArtifact.Tests.ps1 b/tools/tests/VerifyOpenCvArtifact.Tests.ps1 index 450046e..5a396b5 100644 --- a/tools/tests/VerifyOpenCvArtifact.Tests.ps1 +++ b/tools/tests/VerifyOpenCvArtifact.Tests.ps1 @@ -80,11 +80,15 @@ Assert-That ($LASTEXITCODE -eq 0) 'an accepted transitive module (flann) is perm # 行番号は書いた瞬間から古くなる)。**拒んだ相手の名前まで見る。** # # **ここは以前 opencv_dnn500.lib を「許可リストに無い module」の見本にしていたが、 -# `dnn` が Modules に足された 2026-09-08 にその前提が崩れて緑のまま意味を -# 失った**(`$allowed` は `$config.Modules` から動的に作るので、`dnn` は -# もう「外にある」module ではない)。gapi はこのリポジトリのどの計画にも -# 予定が無く、$config.Modules にも $AcceptedTransitiveModules にも現れない -# —— それでも将来 gapi を足す日が来れば、この見本も同じ形で古くなる。 +# `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' From 8957763e09b932fe3d113115db2a7bc42382af83 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 11:51:50 +0900 Subject: [PATCH 10/21] =?UTF-8?q?feat(m7c):=20dnn=20=E3=81=AE=20C=20ABI=20?= =?UTF-8?q?4=20=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **(b) が作った profile の分岐が、実物で初めて働いた** —— spec に "profile": "dnn" を書くと、C# の宣言が Runtime/Interop.Dnn/ の NativeMethodsDnn へ出た。 **engine / backend を選ぶ引数も定数も出していない**(設計 D7)—— 上流の 5.1 で enum EngineType の値が総入れ替えになったので、 5.1 で意味が変わる数字を境界の外へ出さない。 **4 次元の blob は 2 次元に潰す**(決定 A)。分類の 1×N を想定していて、 検出モデルでは N と C の区別が失われる。spec の summary と api-reference の両方に書いた —— 書かないと利用者は検出モデルを 読ませて意味の無い数字を受け取る。ocvu_dnn_blob_from_image の出力は 潰さない(cv::dnn::Net::setInput が要求する 4 次元の NCHW をそのまま 保つ)。 型表が閉じていることを確かめた: ocvu_net_handle を足す前に generate が SpecFormatException で落ちた。 Task 2 のレビューで持ち越された項目を適用した: net_table_slot_capacity() を足し、test_dnn_table_stability.cpp の再配置テストが「4096 個足せば 足りるだろう」という前提ではなく実測に基づくようにした (test_mat_table_stability.cpp と同じ形)—— このテストの net_table を test_dnn.cpp も使い始めた時点で、 表が必ず空から始まるという前提が崩れるため。 COMPONENTS への dnn の追加は Task 2 が先に行っていた (ocvu_dnn_table.cpp の unique_ptr の破棄に必要なため)。 それでも binary は増えず、この 4 本の実装で初めて増えた (Windows debug: 33,565,184 -> 34,873,856 バイト、+1,308,672)。 dev.ps1 test が開始前から赤かった 2 件を閉じた: - OpenCvConfig.Tests.ps1「spec に無いのに Modules に在る: dnn」 - BindingGenerator.Tests.ps1「modules.cmake と bindings/spec の module 一覧が食い違う」 --- .../Interop.Dnn/NativeMethods.Dnn.g.cs | 34 ++++ .../Interop.Dnn/NativeMethods.Dnn.g.cs.meta | 2 + .../generator/Ocvu.Generator/SpecModel.cs | 4 + bindings/spec/dnn.json | 57 ++++++ docs/abi-ownership-and-versioning.md | 159 ++++++++++++++++- docs/api-map.md | 6 +- docs/api-reference.md | 56 +++++- native/include/ocvu/dnn.h | 31 ++++ native/include/opencv_unity_native.h | 10 ++ native/modules.cmake | 8 +- native/src/ocvu_dnn.cpp | 168 ++++++++++++++++++ native/src/ocvu_dnn_table.cpp | 6 + native/src/ocvu_dnn_table.h | 12 ++ native/tests/CMakeLists.txt | 1 + native/tests/test_dnn.cpp | 81 +++++++++ native/tests/test_dnn_table_stability.cpp | 17 ++ native/tests/test_module_linkage.cpp | 12 ++ .../Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs | 32 ++++ .../AbiReachabilityChecks.Dnn.g.cs.meta | 2 + 19 files changed, 682 insertions(+), 16 deletions(-) create mode 100644 Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/NativeMethods.Dnn.g.cs create mode 100644 Packages/com.ayutaz.opencv-unity-native/Runtime/Interop.Dnn/NativeMethods.Dnn.g.cs.meta create mode 100644 bindings/spec/dnn.json create mode 100644 native/include/ocvu/dnn.h create mode 100644 native/src/ocvu_dnn.cpp create mode 100644 native/tests/test_dnn.cpp create mode 100644 tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs create mode 100644 tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs.meta 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..3f2001c --- /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)。width と height は 1 以上 OCVU_DNN_MAX_BLOB_DIM 以下でなければならず(cv::dnn::blobFromImage がその寸法でメモリを確保するため)、外れれば OCVU_STATUS_INVALID_ARGUMENT を返す。mean は NULL を許さず、常に 3 要素(B, G, R の順)を読んで各チャンネルから引く固定契約で、NULL なら OCVU_STATUS_NULL_POINTER。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。src または dst の handle が無効なら OCVU_STATUS_INVALID_HANDLE。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数へは渡さないこと)—— 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, 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 の出力を受け取ることを想定している。net / input / output のいずれかの handle が無効なら OCVU_STATUS_INVALID_HANDLE。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/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..0642a03 --- /dev/null +++ b/bindings/spec/dnn.json @@ -0,0 +1,57 @@ +{ + "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)。width と height は 1 以上 OCVU_DNN_MAX_BLOB_DIM 以下でなければならず(cv::dnn::blobFromImage がその寸法でメモリを確保するため)、外れれば OCVU_STATUS_INVALID_ARGUMENT を返す。mean は NULL を許さず、常に 3 要素(B, G, R の順)を読んで各チャンネルから引く固定契約で、NULL なら OCVU_STATUS_NULL_POINTER。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。src または dst の handle が無効なら OCVU_STATUS_INVALID_HANDLE。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数へは渡さないこと)—— 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", "cType": "const double*", "csType": "double[]", "direction": "in-buffer" }, + { "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 の出力を受け取ることを想定している。net / input / output のいずれかの handle が無効なら OCVU_STATUS_INVALID_HANDLE。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/docs/abi-ownership-and-versioning.md b/docs/abi-ownership-and-versioning.md index c50d844..4d9b8fe 100644 --- a/docs/abi-ownership-and-versioning.md +++ b/docs/abi-ownership-and-versioning.md @@ -261,6 +261,62 @@ 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 だけである。 + +**`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 次元の値を割り当てること自体は合法である —— ただし +`ocvu_mat_get_info` のような 2 次元前提の関数へこの handle を渡した結果は +未定義とする。**4 次元を 2 次元へ潰す決定は、推論の出力側だけに掛かる。** + +**ファイルパスは受けない。** 理由は §1.6 と同じ(Windows の文字コード、 +Android の StreamingAssets)。 + +**ABI version は上げていない。** 新しい module と新しい handle 型を足しただけ +なので `OCVU_ABI_VERSION` は 1 のままである(§2「bump しない変更」)。 + ## 2. C ABI の versioning と後方互換 > **上流の版を上げるときの前提**(2026-08-30 に確認)。OpenCV の @@ -353,11 +409,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 +752,56 @@ 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 要素(B, G, R)の配列である。** `width` / +`height` は buffer の長さではなく `cv::dnn::blobFromImage` がその寸法で +メモリを確保する引数なので、`OCVU_DNN_MAX_BLOB_DIM`(4096)という上限を +新設した —— `cv::cornerSubPix` の `win_size` で踏んだのと同じ形である。 + +**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 +901,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 +933,37 @@ 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`)はまだ無い --- diff --git a/docs/api-map.md b/docs/api-map.md index 4e1b961..dc3f291 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)。width と height は 1 以上 OCVU_DNN_MAX_BLOB_DIM 以下でなければならず(cv::dnn::blobFromImage がその寸法でメモリを確保するため)、外れれば OCVU_STATUS_INVALID_ARGUMENT を返す。mean は NULL を許さず、常に 3 要素(B, G, R の順)を読んで各チャンネルから引く固定契約で、NULL なら OCVU_STATUS_NULL_POINTER。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。src または dst の handle が無効なら OCVU_STATUS_INVALID_HANDLE。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数へは渡さないこと)—— 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 の出力を受け取ることを想定している。net / input / output のいずれかの handle が無効なら OCVU_STATUS_INVALID_HANDLE。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..3585f43 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -15,9 +15,13 @@ §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` は別のマイルストーンで足す)。**詳しい経緯は `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 +428,54 @@ 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`)は別のマイルストーンで追加する +(現時点ではまだ無い)。 + +| 関数 | 何をするか | +| --- | --- | +| `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` の出力だけである。 + +**`ocvu_dnn_blob_from_image` の出力は潰さない。** `cv::dnn::Net::setInput` が +要求するのは 4 次元の NCHW そのものなので、`ocvu_dnn_net_forward` へ渡すまで +形を保つ。この handle を `ocvu_mat_get_info` のような 2 次元前提の関数へ渡した +結果は未定義である。 + +**`mean` は固定 3 要素(B, G, R の順)を読む。** 長さを渡す引数は無く、NULL は +`OCVU_STATUS_NULL_POINTER` になる。`width` / `height` は `OCVU_DNN_MAX_BLOB_DIM` +(4096)以下でなければならない —— buffer の長さではなく、native がその寸法で +メモリを確保する引数だからである(`ocvu_orb_detect` の `max_features` などと +同じ形の上限)。 + +**有効な ONNX を読み込ませて推論する経路は、ここではまだ実証していない。** +壊れた入力に対する振る舞い(NULL、負または 0 の長さ、無効な handle、 +異常な blob の寸法)は L1(`native/tests/test_dnn.cpp`)が固定している。 +実物の小さなモデルを使った正常系は、別のマイルストーンで L3 が見る。 + ### この allowlist に含まれないもの `ocvu_get_abi_version` / `ocvu_get_last_error_status` / `ocvu_get_last_error_message` / diff --git a/native/include/ocvu/dnn.h b/native/include/ocvu/dnn.h new file mode 100644 index 0000000..889b075 --- /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)。width と height は 1 以上 OCVU_DNN_MAX_BLOB_DIM 以下でなければならず(cv::dnn::blobFromImage がその寸法でメモリを確保するため)、外れれば OCVU_STATUS_INVALID_ARGUMENT を返す。mean は NULL を許さず、常に 3 要素(B, G, R の順)を読んで各チャンネルから引く固定契約で、NULL なら OCVU_STATUS_NULL_POINTER。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。src または dst の handle が無効なら OCVU_STATUS_INVALID_HANDLE。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数へは渡さないこと)—— 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, const double* mean, 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 の出力を受け取ることを想定している。net / input / output のいずれかの handle が無効なら OCVU_STATUS_INVALID_HANDLE。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 98d2d86..2f50a9e 100644 --- a/native/include/opencv_unity_native.h +++ b/native/include/opencv_unity_native.h @@ -397,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 が書き出す)。**ここに手で足さないこと。** @@ -415,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 c912754..4d184bf 100644 --- a/native/modules.cmake +++ b/native/modules.cmake @@ -37,10 +37,10 @@ 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) -# **dnn は Task 3 で ocvu_dnn.cpp(C ABI 関数本体)を足すまでこの 1 本だけ。** -# ここに存在しないファイルを書くと configure が落ちる —— handle 表だけを -# 先に固定するのがこの module の Task 2 の役目である。 -set(OCVU_MODULE_dnn src/ocvu_dnn_table.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 が使う。外せる単位から明示的に除く。 diff --git a/native/src/ocvu_dnn.cpp b/native/src/ocvu_dnn.cpp new file mode 100644 index 0000000..d0e0a09 --- /dev/null +++ b/native/src/ocvu_dnn.cpp @@ -0,0 +1,168 @@ +// 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 + +#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 { + std::vector buffer(data, data + length); + auto net = std::make_unique(cv::dnn::readNetFromONNX(buffer)); + 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, const double* mean, int32_t swap_rb, int32_t crop) { + OCVU_TRY_BEGIN + if (mean == nullptr) { + return ::ocvu::set_last_error(OCVU_STATUS_NULL_POINTER, + "ocvu_dnn_blob_from_image: mean is NULL"); + } + // **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[0], mean[1], mean[2]); + 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)); + result = raw.reshape(1, rows); + } 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 index c41be0b..0967223 100644 --- a/native/src/ocvu_dnn_table.cpp +++ b/native/src/ocvu_dnn_table.cpp @@ -79,6 +79,12 @@ cv::dnn::Net* net_table_get(ocvu_net_handle handle) { 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; } diff --git a/native/src/ocvu_dnn_table.h b/native/src/ocvu_dnn_table.h index 250883d..3e0346b 100644 --- a/native/src/ocvu_dnn_table.h +++ b/native/src/ocvu_dnn_table.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -20,4 +21,15 @@ 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/tests/CMakeLists.txt b/native/tests/CMakeLists.txt index 82feea0..c2a671c 100644 --- a/native/tests/CMakeLists.txt +++ b/native/tests/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable(ocvu_tests 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..8dedc5a --- /dev/null +++ b/native/tests/test_dnn.cpp @@ -0,0 +1,81 @@ +// **有効な ONNX を手で組むのは現実的でない。** だからこのテストは +// **壊れた入力に対する振る舞い**を固定する。正常系は L3 が、 +// 実物の小さなモデルを使って見る(Task 5)。 +#include +#include +#include + +#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); + + const double mean[3] = {0.0, 0.0, 0.0}; + EXPECT_EQ( + ocvu_dnn_blob_from_image(src, blob, 1.0, 1 << 20, 1 << 20, mean, 0, 0), + OCVU_STATUS_INVALID_ARGUMENT); + EXPECT_EQ( + ocvu_dnn_blob_from_image(src, blob, 1.0, 0, 8, mean, 0, 0), + OCVU_STATUS_INVALID_ARGUMENT); + + ocvu_mat_release(src); + ocvu_mat_release(blob); +} diff --git a/native/tests/test_dnn_table_stability.cpp b/native/tests/test_dnn_table_stability.cpp index 2f0c87a..ee092f4 100644 --- a/native/tests/test_dnn_table_stability.cpp +++ b/native/tests/test_dnn_table_stability.cpp @@ -7,6 +7,14 @@ // **ローカル 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 @@ -20,12 +28,21 @@ TEST(DnnTableStability, APointerStaysValidWhileTheTableGrows) { 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()); // 触って落ちないこと 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/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..f5fe76e --- /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, default, 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 From a007301c5b8088bf5512311668fae56577093a23 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 12:29:39 +0900 Subject: [PATCH 11/21] =?UTF-8?q?fix(m7c):=20dnn=20=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=201=20=E5=B7=A1=E7=9B=AE=E3=81=AE=20Critical?= =?UTF-8?q?=201=20/=20Important=203=20/=20Minor=204=20=E3=82=92=E7=9B=B4?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical 1: mean を const double* + 長さ無しの buffer から、mean_b / mean_g / mean_r の 3 個の scalar に変える。配列 + 長さ無しの形は、短い配列を渡せば 境界の外を読めるという誤りを検査ではなく summary の文章だけで防いでいた。 3 個の scalar にすることで、その誤りは表現できなくなった。NULL の心配も 消えた。spec を書き換えて generate し直した(LibraryName の複製など既存の 生成物には影響なし)。 Important 1: ocvu_mat_get_info が dims > 2 の Mat を拒むようになった。 ocvu_dnn_blob_from_image が作る 4 次元の blob をそのまま渡すと、OpenCV が 返す rows = cols = -1 を素通しして OCVU_STATUS_OK のまま負の total_bytes を 返していた——「未定義」ではなく「間違った値を自信満々で返す」形で、C# から 到達可能だった。native/tests/test_mat_lifecycle.cpp の white-box テスト (mat_table_acquire で直接 3 次元の Mat を作る)で先に赤くしてから直した。 Important 2: ocvu_dnn_net_forward の出力を .clone() する。Net::forward() が 返す Mat は net 内部のバッファへの浅いコピーで、reshape もヘッダだけを 作り直す浅い操作なので、clone を省くと同じ net で 2 回目の forward を 呼んだときに 1 回目の出力が黙って書き換わる。実物の ONNX で forward を 2 回呼ぶ検証はまだ無く、後続タスクの担当として明記した。 Important 3: net_table_slot_capacity() の容量アサーションに、初めての 負の対照を取った(free list を温めて再配置を止めると ASSERT_GT が実際に 落ちることを確認し、戻した)。 Minor 1: ocvu_dnn_blob_from_image の検証順序(width/height → src handle → dst handle)を summary に明記し、両方を同時に破る入力で順序をロックする L1 を足した。 Minor 2: blob_from_image の無効な src/dst handle、forward の無効な input/output handle、解放済み net への forward の 6 件を L1 に追加した。 解放済み net のケースは net_table_add で作った合成 net を公開の ocvu_dnn_net_release で解放してから forward を呼ぶ、実際の使用順序を 再現している——実物の ONNX を使った確認は後続タスクの担当。 Minor 3: ocvu_dnn_net_read_onnx が std::vector へコピーせず、 cv::dnn::readNetFromONNX(const char*, size_t) を直接呼ぶようにした (100 MB 級のモデルでピークメモリが倍になるのを避ける)。 Minor 4: §2 に ocvu_net_handle が bump に当たらない実例への言及を足した。 test-native: 219 (Task 3 開始時) → 234(+15: dnn 6 件 + mat_lifecycle 1 件 + Task 3 の 8 件)。test-asan: 5/5。dev.ps1 test: exit 0。 plugin size (Windows debug): 34,873,856 → 34,869,248(-4,608、ほぼ相殺)。 --- .../Interop.Dnn/NativeMethods.Dnn.g.cs | 6 +- bindings/spec/dnn.json | 8 +- docs/abi-ownership-and-versioning.md | 71 ++++++++++++-- docs/api-map.md | 4 +- docs/api-reference.md | 27 +++-- native/include/ocvu/dnn.h | 6 +- native/src/ocvu_dnn.cpp | 50 +++++++--- native/src/ocvu_mat.cpp | 12 +++ native/tests/test_dnn.cpp | 98 ++++++++++++++++++- native/tests/test_mat_lifecycle.cpp | 36 +++++++ .../Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs | 2 +- 11 files changed, 273 insertions(+), 47 deletions(-) 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 index 3f2001c..56a3932 100644 --- 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 @@ -22,11 +22,11 @@ internal static partial class NativeMethodsDnn [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)。width と height は 1 以上 OCVU_DNN_MAX_BLOB_DIM 以下でなければならず(cv::dnn::blobFromImage がその寸法でメモリを確保するため)、外れれば OCVU_STATUS_INVALID_ARGUMENT を返す。mean は NULL を許さず、常に 3 要素(B, G, R の順)を読んで各チャンネルから引く固定契約で、NULL なら OCVU_STATUS_NULL_POINTER。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。src または dst の handle が無効なら OCVU_STATUS_INVALID_HANDLE。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数へは渡さないこと)—— 2 次元へ潰すのは ocvu_dnn_net_forward の出力側だけである。OpenCV が例外を投げた場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは dst を書き換えない。 + /// 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, int swap_rb, int crop); + 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 の出力を受け取ることを想定している。net / input / output のいずれかの handle が無効なら OCVU_STATUS_INVALID_HANDLE。OpenCV が例外を投げた場合、または結果が空になった場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは output を書き換えない。engine / backend を選ぶ引数は無い(上流の 5.1 で enum EngineType の値が総入れ替えになったため、5.1 で意味が変わる数字を境界の外へ出さない)。 + /// 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/bindings/spec/dnn.json b/bindings/spec/dnn.json index 0642a03..154f61f 100644 --- a/bindings/spec/dnn.json +++ b/bindings/spec/dnn.json @@ -26,7 +26,7 @@ }, { "name": "ocvu_dnn_blob_from_image", - "summary": "src を推論の入力形式(4 次元の blob。N=1, C, height, width)に変換して dst に書く。scale は画素値に掛ける係数(例: 1/255.0)。width と height は 1 以上 OCVU_DNN_MAX_BLOB_DIM 以下でなければならず(cv::dnn::blobFromImage がその寸法でメモリを確保するため)、外れれば OCVU_STATUS_INVALID_ARGUMENT を返す。mean は NULL を許さず、常に 3 要素(B, G, R の順)を読んで各チャンネルから引く固定契約で、NULL なら OCVU_STATUS_NULL_POINTER。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。src または dst の handle が無効なら OCVU_STATUS_INVALID_HANDLE。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数へは渡さないこと)—— 2 次元へ潰すのは ocvu_dnn_net_forward の出力側だけである。OpenCV が例外を投げた場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは dst を書き換えない。", + "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, @@ -36,14 +36,16 @@ { "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", "cType": "const double*", "csType": "double[]", "direction": "in-buffer" }, + { "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 の出力を受け取ることを想定している。net / input / output のいずれかの handle が無効なら OCVU_STATUS_INVALID_HANDLE。OpenCV が例外を投げた場合、または結果が空になった場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは output を書き換えない。engine / backend を選ぶ引数は無い(上流の 5.1 で enum EngineType の値が総入れ替えになったため、5.1 で意味が変わる数字を境界の外へ出さない)。", + "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, diff --git a/docs/abi-ownership-and-versioning.md b/docs/abi-ownership-and-versioning.md index 4d9b8fe..9f8ed18 100644 --- a/docs/abi-ownership-and-versioning.md +++ b/docs/abi-ownership-and-versioning.md @@ -307,15 +307,27 @@ M7 節)ので、5.1 で意味が変わる数字を境界の外へ出さない **`ocvu_dnn_blob_from_image` の出力は潰さない。** `cv::dnn::Net::setInput` が 要求するのは 4 次元の NCHW そのものなので、`ocvu_dnn_net_forward` へ渡すまで 形を保つ。`cv::Mat` は内部で 2 次元より多い次元を表現できるので、この -handle へ 4 次元の値を割り当てること自体は合法である —— ただし -`ocvu_mat_get_info` のような 2 次元前提の関数へこの handle を渡した結果は -未定義とする。**4 次元を 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_ABI_VERSION` は 1 のままである(§2「bump しない変更」)。 +**ABI version は上げていない。** 新しい module と新しい handle 型 +(`ocvu_net_handle`)を足しただけなので `OCVU_ABI_VERSION` は 1 のままである +—— この判断の一般原則(「新しい関数を足す」「新しい status を末尾に足す」は +bump しない)は §2「bump しない変更」が正本として持つ。 ## 2. C ABI の versioning と後方互換 @@ -364,6 +376,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」と区別が付かない)ではなくこの新しい @@ -787,10 +805,45 @@ P/Invoke 宣言だけが別 assembly(`CvUnity.Interop.Dnn` の `NativeMethodsD `enum EngineType` の値が総入れ替えになった(§2 冒頭の引用)ので、5.1 で 意味が変わる数字を境界の外へ出さない。 -**`mean` は長さを渡さない固定 3 要素(B, G, R)の配列である。** `width` / -`height` は buffer の長さではなく `cv::dnn::blobFromImage` がその寸法で -メモリを確保する引数なので、`OCVU_DNN_MAX_BLOB_DIM`(4096)という上限を -新設した —— `cv::cornerSubPix` の `win_size` で踏んだのと同じ形である。 +**`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 のヘッダに明記が無く、 +実装から推測している。** 実物の ONNX モデルで forward を 2 回呼んで +確かめるのは、まだ無いので後続タスクの担当である。 + +**`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` の定義が要るため、 diff --git a/docs/api-map.md b/docs/api-map.md index dc3f291..a0481f8 100644 --- a/docs/api-map.md +++ b/docs/api-map.md @@ -38,8 +38,8 @@ | `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)。width と height は 1 以上 OCVU_DNN_MAX_BLOB_DIM 以下でなければならず(cv::dnn::blobFromImage がその寸法でメモリを確保するため)、外れれば OCVU_STATUS_INVALID_ARGUMENT を返す。mean は NULL を許さず、常に 3 要素(B, G, R の順)を読んで各チャンネルから引く固定契約で、NULL なら OCVU_STATUS_NULL_POINTER。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。src または dst の handle が無効なら OCVU_STATUS_INVALID_HANDLE。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数へは渡さないこと)—— 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 の出力を受け取ることを想定している。net / input / output のいずれかの handle が無効なら OCVU_STATUS_INVALID_HANDLE。OpenCV が例外を投げた場合、または結果が空になった場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは output を書き換えない。engine / backend を選ぶ引数は無い(上流の 5.1 で enum EngineType の値が総入れ替えになったため、5.1 で意味が変わる数字を境界の外へ出さない)。 | +| `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 3585f43..d8866c4 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -458,18 +458,27 @@ blob(NCHW)だが、この ABI の `Mat` は `rows` / `cols` / `channels` / ` しか公開していない。`ocvu_dnn_net_forward` は最後の次元を列数として残りを すべて行数へ畳み込む —— **分類モデルの典型的な `(1, N)` はそのまま `1 × N` に なるが、検出モデルのような 4 次元の出力では N と C の区別が失われる。** -この ABI が受け取れるのは分類モデルの `1 × N` の出力だけである。 +この 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 次元前提の関数へ渡した -結果は未定義である。 - -**`mean` は固定 3 要素(B, G, R の順)を読む。** 長さを渡す引数は無く、NULL は -`OCVU_STATUS_NULL_POINTER` になる。`width` / `height` は `OCVU_DNN_MAX_BLOB_DIM` -(4096)以下でなければならない —— buffer の長さではなく、native がその寸法で -メモリを確保する引数だからである(`ocvu_orb_detect` の `max_features` などと -同じ形の上限)。 +形を保つ。この 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 を読み込ませて推論する経路は、ここではまだ実証していない。** 壊れた入力に対する振る舞い(NULL、負または 0 の長さ、無効な handle、 diff --git a/native/include/ocvu/dnn.h b/native/include/ocvu/dnn.h index 889b075..b7dbe41 100644 --- a/native/include/ocvu/dnn.h +++ b/native/include/ocvu/dnn.h @@ -18,10 +18,10 @@ OCVU_API ocvu_status ocvu_dnn_net_read_onnx(const uint8_t* data, int32_t length, /* 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)。width と height は 1 以上 OCVU_DNN_MAX_BLOB_DIM 以下でなければならず(cv::dnn::blobFromImage がその寸法でメモリを確保するため)、外れれば OCVU_STATUS_INVALID_ARGUMENT を返す。mean は NULL を許さず、常に 3 要素(B, G, R の順)を読んで各チャンネルから引く固定契約で、NULL なら OCVU_STATUS_NULL_POINTER。swap_rb は 0 以外で R と B を入れ替え、crop は 0 以外でアスペクト比を保ったまま中央をトリミングする。src または dst の handle が無効なら OCVU_STATUS_INVALID_HANDLE。dst が受け取る Mat は 4 次元のまま保持される(ocvu_mat_get_info のような 2 次元前提の関数へは渡さないこと)—— 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, const double* mean, int32_t swap_rb, int32_t crop); +/* 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 の出力を受け取ることを想定している。net / input / output のいずれかの handle が無効なら OCVU_STATUS_INVALID_HANDLE。OpenCV が例外を投げた場合、または結果が空になった場合は OCVU_STATUS_OPENCV_ERROR を返し、失敗したときは output を書き換えない。engine / backend を選ぶ引数は無い(上流の 5.1 で enum EngineType の値が総入れ替えになったため、5.1 で意味が変わる数字を境界の外へ出さない)。 */ +/* 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 diff --git a/native/src/ocvu_dnn.cpp b/native/src/ocvu_dnn.cpp index d0e0a09..1f3942e 100644 --- a/native/src/ocvu_dnn.cpp +++ b/native/src/ocvu_dnn.cpp @@ -27,7 +27,6 @@ #include #include -#include #include "ocvu_dnn_table.h" #include "ocvu_error.h" @@ -48,8 +47,15 @@ extern "C" ocvu_status ocvu_dnn_net_read_onnx(const uint8_t* data, int32_t lengt // OPENCV_ERROR を返したいので、ここで自分で捕まえる // (ocvu_stereo.cpp / ocvu_calibration.cpp と同じ作法)。 try { - std::vector buffer(data, data + length); - auto net = std::make_unique(cv::dnn::readNetFromONNX(buffer)); + // **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 が空のネットワークになった"); @@ -73,16 +79,22 @@ extern "C" ocvu_status ocvu_dnn_net_release(ocvu_net_handle net) { 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, const double* mean, int32_t swap_rb, int32_t crop) { +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 - if (mean == nullptr) { - return ::ocvu::set_last_error(OCVU_STATUS_NULL_POINTER, - "ocvu_dnn_blob_from_image: mean is NULL"); - } - // **width / height は buffer の長さではなく、native(cv::dnn::blobFromImage)が - // その寸法でメモリを確保する引数である。** cv::cornerSubPix の win_size で - // 踏んだのと同じ形なので上限を置く(add-abi-function skill の - // 「buffer ではないのに上限が要る引数」)。 + // **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( @@ -105,7 +117,7 @@ extern "C" ocvu_status ocvu_dnn_blob_from_image(ocvu_mat_handle src, ocvu_mat_ha // (ocvu_stereo.cpp / ocvu_calibration.cpp と同じ作法)。 cv::Mat result; try { - const cv::Scalar mean_scalar(mean[0], mean[1], mean[2]); + 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) { @@ -157,7 +169,17 @@ extern "C" ocvu_status ocvu_dnn_net_forward(ocvu_net_handle net, ocvu_mat_handle "ocvu_dnn_net_forward: forward() produced a degenerate shape"); } const int rows = static_cast(raw.total() / static_cast(last_dim)); - result = raw.reshape(1, rows); + // **`.clone()` が要る(レビュー指摘、Important 2)。** `Net::forward()` + // が返す Mat はネットワーク内部のバッファへの浅いコピーで、 + // `reshape` もヘッダだけを作り直す浅い操作である。`.clone()` を + // 省くと `*output_mat` は net が所有するメモリを指したままになり、 + // 同じ net で 2 回目の forward を呼ぶと 1 回目の出力が黙って + // 書き換わる —— この ABI の他のすべての関数が新しく確保した + // メモリを返しているのに、ここだけが例外になってしまう。 + // **未確認事項**: この所有権の挙動は OpenCV のヘッダには明記が + // 無く、実装から推測している。実物の ONNX モデルで forward を + // 2 回呼んで検証するのは、まだ無いので後続タスクの担当である。 + result = raw.reshape(1, rows).clone(); } catch (const cv::Exception& e) { return ::ocvu::set_last_error(OCVU_STATUS_OPENCV_ERROR, e.what()); } 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/test_dnn.cpp b/native/tests/test_dnn.cpp index 8dedc5a..eae3539 100644 --- a/native/tests/test_dnn.cpp +++ b/native/tests/test_dnn.cpp @@ -3,8 +3,20 @@ // 実物の小さなモデルを使って見る(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) { @@ -68,14 +80,94 @@ TEST(Dnn, AnAbsurdBlobSizeIsRejectedRatherThanAttempted) { 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); - const double mean[3] = {0.0, 0.0, 0.0}; EXPECT_EQ( - ocvu_dnn_blob_from_image(src, blob, 1.0, 1 << 20, 1 << 20, mean, 0, 0), + 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, mean, 0, 0), + 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_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/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs b/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs index f5fe76e..fad3575 100644 --- a/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs +++ b/tests/UnityProject/Assets/Tests/Shared.Dnn/AbiReachabilityChecks.Dnn.g.cs @@ -25,7 +25,7 @@ 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, default, 0, 0); + 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; } From 360b4ca3134afae0bd0b5089a2be1a800cda8b53 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 13:07:36 +0900 Subject: [PATCH 12/21] =?UTF-8?q?build(m7c):=20arm64=20=E3=81=A7=20libpng?= =?UTF-8?q?=20=E3=81=AE=20ARM=20NEON=20=E3=82=92=E5=88=87=E3=82=8A?= =?UTF-8?q?=E3=80=81dnn/mlas=20=E3=81=AE=20ASM=20=E7=84=A1=E5=8A=B9?= =?UTF-8?q?=E5=8C=96=E3=82=92=E5=8A=B9=E3=81=8B=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS arm64 で opencv_unity_native.dylib のリンクが 「MlasHGemmSupported が未解決」で落ちていた(PR #72、run 34305540718)。 根本原因は上流 OpenCV 5.0.0 の vendoring 欠陥(3rdparty/mlas/lib/compute.cpp の明示的テンプレート特殊化 MlasGQASupported が、vendor して いない HGEMM 実装を無条件に要求する)だが、mlas 自身は「plain ASM が 使えなければ大人しく return() する」という正しい escape を既に持っており、 x86_64(Windows/Linux)は実際にそちらを通って MLAS ごと無効化されている (`DNN MLAS: NO (no ASM compiler available for ...)`)。 共通の CMakeArgs にある -DCMAKE_ASM_COMPILER=NOTFOUND は、この escape を 引くための唯一のこちら側の入力である。ところが arm64 の 3 platform (Android / macOS / iOS)だけ、mlas の判定に届く前に libpng が plain ASM を本物にしてしまい(3rdparty/libpng/CMakeLists.txt の PNG_ARM_NEON、既定 "on"、enable_language(ASM) + arm/filter_neon.S を 追加する)、pin が意味を持たなくなっていた。実測(configure ログ)で Android/macOS/iOS の 3 platform とも「libpng の HAVE_VLD1_* 判定 → NEON intrinsics 採用のメッセージ → ASM compiler identification」という 同一の並びを確認し、他のどの component もその間に挟まっていないことを フィルタ無しの生ログで確かめた。x64 は別の CMake 言語(ASM_NASM、 libjpeg-turbo の x86 SIMD が使う。CMAKE_ASM_COMPILER とは無関係な変数)を 使うため、この pin が素通しで効く。 -DPNG_ARM_NEON=off を arm64 の 3 platform にだけ足す。libjpeg-turbo と carotene/kleidicv も候補として検討したが、libjpeg-turbo は add_subdirectory(simd) が SIMD extensions: の行より前に完了しており ログの並びと矛盾し、carotene/kleidicv は enable_language の呼び出しが ソース中に見当たらなかった。libpng だけが実測と矛盾しない。 **代償**: PNG の ARM NEON デコード高速化を Android/macOS/iOS の 3 platform で失う。OpenCV 全体の WITH_SIMD(core/imgproc)や libjpeg-turbo の JPEG SIMD には触れない。x64/web-wasm の構成ハッシュは変わらない (windows-x64 = c21fe00b8952、linux-x64 = 149b4b28a51c、 web-wasm = a78f69f65591、いずれも変更前と同一)。 構成ハッシュ: macos-arm64 95012c8d5ebc -> 632e55a2ce20、 android-arm64 be6c6fe2b8ec -> 6652b63325ca、 ios-arm64 8967a5286e75 -> 0f7bbdf12bdb。 6 platform 分の OpenCV 再ビルドが必要 (build-opencv.yml が tools/opencv-config.psd1 の変更で自動起動する)。 まだ実証していない: この変更で実際に「DNN MLAS: NO」になるか、 libopencv_dnn.a から .S.o が消えるか、tools/verify-artifact-linkage.ps1 が実物の artifact に対して green になるかは、この commit の push が 起こす build-opencv.yml の実行結果を見て確認する。 --- tools/opencv-config.psd1 | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tools/opencv-config.psd1 b/tools/opencv-config.psd1 index d11a8e2..503450a 100644 --- a/tools/opencv-config.psd1 +++ b/tools/opencv-config.psd1 @@ -126,6 +126,48 @@ # 配布先の下限を固定する。指定しないとビルドマシンの 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 が無い」経路を取れるようになるはず、 + という仮説にもとづく。**代償は明確: 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 +197,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 +225,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' ) <# From 21e00b966af9e111b45a0a061366b72c06b13181 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 14:27:21 +0900 Subject: [PATCH 13/21] =?UTF-8?q?feat(m7c):=20dnn=20=E3=81=AE=20C#=20?= =?UTF-8?q?=E5=85=AC=E9=96=8B=20API=20=E3=81=A8=E3=80=81Unity=20=E3=81=A7?= =?UTF-8?q?=E5=88=87=E3=82=8C=E3=82=8B=E3=81=93=E3=81=A8=E3=81=AE=E5=AE=9F?= =?UTF-8?q?=E8=A8=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **公開 API 層にも defineConstraints を置く。** Interop.Dnn だけを切ると、 それを参照する CvUnity.Dnn が参照先を失って利用者のプロジェクトが コンパイルエラーになる。CvNet(IDisposable、CvMat と同じ寿命契約)と CvDnn(ReadOnnx / BlobFromImage / Forward)を Runtime/Dnn/ に足した。 **CvUnity.Core に初めて AssemblyInfo.cs を置いた。** CvDnn は CvMat.Handle (internal)を別 assembly から読む最初の消費者で、これは L3(shim が 全部を 1 つの assembly にまとめる)では見えない穴だった —— Unity の中で define を立てて実際にコンパイルさせて初めて表面化した。 **L3 は常に dnn を見る**(.csproj は asmdef の制約を受けない)。shim に Interop.Dnn / Dnn を足して意図的にそうしてある —— L3 で常に検証しておき、 Unity 側では切れることを別に確かめる(ProfileGatingTests、両方向を実測)。 docs/api-reference.md §2.17 と docs/abi-ownership-and-versioning.md §4 を 同じ commit で更新した。 --- .../Runtime/Core/AssemblyInfo.cs | 13 ++ .../Runtime/Core/AssemblyInfo.cs.meta | 2 + .../Runtime/Dnn.meta | 8 + .../Runtime/Dnn/CvDnn.cs | 158 ++++++++++++++++++ .../Runtime/Dnn/CvDnn.cs.meta | 2 + .../Runtime/Dnn/CvUnity.Dnn.asmdef | 16 ++ .../Runtime/Dnn/CvUnity.Dnn.asmdef.meta | 7 + .../Runtime/Interop.Dnn/AssemblyInfo.cs | 23 ++- docs/abi-ownership-and-versioning.md | 29 ++++ docs/api-reference.md | 55 +++++- .../CvUnity.Runtime.Shim.csproj | 8 +- .../Managed/CvUnity.Tests.Managed/DnnTests.cs | 44 +++++ .../Tests/EditMode/ProfileGatingTests.cs | 40 ++++- 13 files changed, 385 insertions(+), 20 deletions(-) create mode 100644 Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs create mode 100644 Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs.meta create mode 100644 Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn.meta create mode 100644 Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvDnn.cs create mode 100644 Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvDnn.cs.meta create mode 100644 Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef create mode 100644 Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef.meta create mode 100644 tests/Managed/CvUnity.Tests.Managed/DnnTests.cs 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..bbf96c9 --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs @@ -0,0 +1,13 @@ +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 と同じ方針)。 +[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..668624e --- /dev/null +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef @@ -0,0 +1,16 @@ +{ + "name": "CvUnity.Dnn", + "rootNamespace": "CvUnity.Dnn", + "references": [ + "CvUnity.Interop", + "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/docs/abi-ownership-and-versioning.md b/docs/abi-ownership-and-versioning.md index 9f8ed18..dd10b8b 100644 --- a/docs/abi-ownership-and-versioning.md +++ b/docs/abi-ownership-and-versioning.md @@ -1018,6 +1018,35 @@ platform(iOS / WebGL)で `"__Internal"` にならず、Player の中で最 モデルを使う 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 の担当である。** + --- ## 参照 diff --git a/docs/api-reference.md b/docs/api-reference.md index d8866c4..dc85400 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -18,7 +18,9 @@ M4 で足したので §2.6 にある。QR コードの符号化・復号と ORB features / geometry / カメラ校正」と §2.8〜§2.11 にある。**M7c で `dnn` を 4 本足した** —— C ABI は §1「dnn」にあるが、`"profile": "dnn"` を宣言する 初めての module なので C# の宣言だけが別 assembly(`CvUnity.Interop.Dnn`)へ -出る(利用者向けの `CvUnity.Dnn.CvDnn` は別のマイルストーンで足す)。**詳しい経緯は +出る。**利用者向けの `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・§1.7 を、profile の 仕組みは同 §4 を参照。 @@ -435,8 +437,8 @@ SIFT は `create(200)` で 240 個)。**`capacity` を `max_features` と同 `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`)は別のマイルストーンで追加する -(現時点ではまだ無い)。 +**利用者側の公開 API(`CvUnity.Dnn.CvDnn` / `CvNet`)は M7c Task 4 で足した** +—— §2.17 にある。 | 関数 | 何をするか | | --- | --- | @@ -482,8 +484,10 @@ width / height の範囲 → src の handle → dst の handle で固定して **有効な ONNX を読み込ませて推論する経路は、ここではまだ実証していない。** 壊れた入力に対する振る舞い(NULL、負または 0 の長さ、無効な handle、 -異常な blob の寸法)は L1(`native/tests/test_dnn.cpp`)が固定している。 -実物の小さなモデルを使った正常系は、別のマイルストーンで L3 が見る。 +異常な blob の寸法)は L1(`native/tests/test_dnn.cpp`)と、公開 API の +壊れた入力・寿命・所有権(`tests/Managed/CvUnity.Tests.Managed/DnnTests.cs`。 +§2.17)が固定している。実物の小さなモデルを使った正常系は、後続のタスクが +L3 に足す。 ### この allowlist に含まれないもの @@ -502,9 +506,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` @@ -948,6 +955,38 @@ 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 モデルを読んで実際に推論する経路)は、この commit ではまだ +実証していない。** `DnnTests.cs`(`tests/Managed/CvUnity.Tests.Managed/`)が見るのは +壊れた入力・寿命・所有権だけで、有効な ONNX を手で組むのは現実的でないため +正常系は別のタスクが実物の小さなモデルを使って L3 に足す。 + ## 3. 対象外(この文書に書かないもの) `Mat` の部分参照(ROI)、型変換・算術演算、**`imgcodecs` のファイルパス 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/DnnTests.cs b/tests/Managed/CvUnity.Tests.Managed/DnnTests.cs new file mode 100644 index 0000000..2cc17a3 --- /dev/null +++ b/tests/Managed/CvUnity.Tests.Managed/DnnTests.cs @@ -0,0 +1,44 @@ +using System; +using CvUnity; +using CvUnity.Dnn; +using Xunit; + +/// +/// **有効な 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/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs b/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs index 4d1cef6..1230796 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,26 @@ public void TheDnnSharedTestAssemblyIsAbsentWithoutItsDefine() "defineConstraints が効いていない"); } + /// + /// **dnn の公開 API 層も、define が無ければコンパイルされない。** + /// + /// Interop.Dnn だけを切っても足りない —— それを参照する + /// CvUnity.Dnn が残ると、参照先を失って**利用者のプロジェクトが + /// コンパイルエラーになる。** + /// + [Test] + public void TheDnnPublicApiAssemblyIsAlsoAbsentWithoutItsDefine() + { + 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 +177,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, From 45606d99e33745802d11625316fc714da732ee43 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 14:56:00 +0900 Subject: [PATCH 14/21] =?UTF-8?q?test(m7c):=20=E5=AE=9F=E7=89=A9=E3=81=AE?= =?UTF-8?q?=20ONNX=20=E3=82=92=E9=80=9A=E3=81=97=E3=81=A6=E5=A2=83?= =?UTF-8?q?=E7=95=8C=E3=81=AE=E5=BE=80=E5=BE=A9=E3=82=92=E7=A2=BA=E3=81=8B?= =?UTF-8?q?=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **モデルは外から取ってこず、自分で作った** —— 外部のモデルは ライセンスの確認が要り、リポジトリに数 MB が入り、上流が消すと CI が壊れる。 Identity 1 ノードの最小の ONNX なので、**推論の中身は何もしない。** 確かめているのは「読めて、blob になって、forward が返る」ことである。 **生成スクリプトをモデルの隣に置いた(tiny.onnx.py)。** モデルは binary なので中身が読めず、作り方が残っていないと opset を上げたくなったときに 作り直せない。ライセンスは THIRD_PARTY_NOTICES.md に足していない —— 自作の合成物(重みも構造も無い恒等写像)に third party の権利は無い。 brief の提示テストは CvMatType.Rgb24 / Gray32F という存在しない名前を 使っていたので、実際の enum(Bgr24 = 8UC3, Response32 = 32FC1)に直した。 BlobFromImageProducesTheRequestedShape も見直した —— blob は 4 次元の まま保持される契約で、2 次元前提の CvMat.Rows は ocvu_mat_get_info が INVALID_ARGUMENT で拒む(native 側で dims > 2 を明示的に断っている)ので、 brief のとおり blob.Rows * blob.Cols を読もうとすると値ではなく例外が返る。 その拒否自体を検証する形(BlobFromImageKeepsTheBlobFourDimensional)に 書き直し、要素数の確認は Forward を通した 2 次元の出力側で行う。 Task 4 レビューからの持ち越しを 2 件追加した: - DisposingACvNetTwiceIsSafe: 実物の CvNet を C# レベルで二重 Dispose する。 DnnTests.DisposingTwiceIsSafe は有効な net を作れず native を直接 叩いていたが、ここでは CvNet.Dispose() のガードそのものを通す。 - CallingForwardTwiceDoesNotRewriteTheFirstOutput: ocvu_dnn_net_forward の .clone() が無いと 1 回目の出力が 2 回目の forward で書き換わる という、Task 3 で直した defect の再発防止。 dev.ps1 test-managed: CvUnity.Tests.Managed 189 -> 195 (+6)、 Ocvu.Generator.Tests 116(変化なし)、exit 0。 --- .../CvUnity.Tests.Managed.csproj | 7 + .../DnnInferenceTests.cs | 151 ++++++++++++++++++ .../TestModels/tiny.onnx | Bin 0 -> 110 bytes .../TestModels/tiny.onnx.py | 38 +++++ 4 files changed, 196 insertions(+) create mode 100644 tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs create mode 100644 tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx create mode 100644 tests/Managed/CvUnity.Tests.Managed/TestModels/tiny.onnx.py 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..cf7359a --- /dev/null +++ b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs @@ -0,0 +1,151 @@ +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 の規約) + } + + /// + /// **Task 3 が直した defect の再発防止。** ocvu_dnn_net_forward が + /// 返す Mat は元々 net 内部バッファへの浅いコピーで、同じ net に対する + /// 2 回目の forward が 1 回目の出力を黙って書き換えていた。修正は + /// .clone() を足すことだったが、それを実物のモデルで確かめたのは + /// ここが最初である。 + /// + [Fact] + public void CallingForwardTwiceDoesNotRewriteTheFirstOutput() + { + 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 firstOutput = CvMat.Create(1, 1, CvMatType.Response32); + using var secondOutput = 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, firstOutput); + + var beforeSecondForward = ReadAll(firstOutput); + + // **同じ net で 2 回目の forward を呼ぶ。** 別の handle + // (secondOutput) に書かせるが、defect が再発していれば + // net が内部で持つバッファ経由で firstOutput まで書き換わる。 + CvDnn.Forward(net, blob, secondOutput); + + var afterSecondForward = ReadAll(firstOutput); + + Assert.Equal(beforeSecondForward, afterSecondForward); + } + + /// 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; + } +} 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 0000000000000000000000000000000000000000..1baf31e972a3bd9c5299c73d949733591ad88eb6 GIT binary patch literal 110 zcmdlD0$fZS aj6lo`#4I43q`(E$DuhFY6AKrE051SiauY8A literal 0 HcmV?d00001 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) From 7411bae19efba757570268d6212714f2d3d394d8 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 15:04:02 +0900 Subject: [PATCH 15/21] =?UTF-8?q?fix(m7c):=20=E4=BA=8C=E9=87=8D=20forward?= =?UTF-8?q?=20=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E3=81=97=E3=80=81=E8=B2=A0=E3=81=AE=E5=AF=BE=E7=85=A7?= =?UTF-8?q?=E3=81=8C=E5=8F=96=E3=82=8C=E3=81=AA=E3=81=84=E3=81=93=E3=81=A8?= =?UTF-8?q?=E3=82=92=E8=A8=98=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **最初の実装は同じ blob を 2 回の forward に渡していた** —— Identity(x) は入力の写しに過ぎないので、defect の有無に関わらず 1 回目と 2 回目の値が一致してしまい、何も検証していなかった。 srcA / srcB という別々の入力から別々の blob を作り、2 回目の forward が確かに異なる結果を生むこと(Assert.NotEqual による前提の 健全性チェック)まで確かめた上で、1 回目の出力が書き換わらないことを 見るようにした。 **その上で、native/src/ocvu_dnn.cpp の .clone() を一時的に外して 負の対照を取ろうとしたが、取れなかった。** Identity 1 ノードでも、 learned weight を持たない 2 層の Relu モデル(投げ捨てた検証用) でも、.clone() の有無でこのテストの結果は変わらなかった —— 疑って いるのは、*output_mat = result の代入で mat_table 側の Slot が result のデータへの参照を持ち続けるため、2 回目の forward が同じ 内部バッファへ cv::Mat::create() で書こうとしても参照が生きている 限り再割り当てが起きる、という経路だが未確認である。 CLAUDE.md の prove-a-check-works の節が M6 で記録した教訓 (「負の対照がそもそも取れないことがある」「書けないと書く」)と 同じ形をここでも踏んだので、docstring に実測として残した。 このテストは契約どおりの結果を実物のモデルで確認する positive な 回帰検査ではあるが、.clone() を消しても今のところ検知できない ——「動くことを確かめた」と「defect を再現させて潰したことを 証明した」は別である。 dev.ps1 test-managed: CvUnity.Tests.Managed 195/195、 Ocvu.Generator.Tests 116/116、exit 0(変化なし、ロジックの 修正のみ)。 --- .../DnnInferenceTests.cs | 73 ++++++++++++++----- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs index cf7359a..19e6265 100644 --- a/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs +++ b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs @@ -106,37 +106,66 @@ public void DisposingACvNetTwiceIsSafe() } /// - /// **Task 3 が直した defect の再発防止。** ocvu_dnn_net_forward が - /// 返す Mat は元々 net 内部バッファへの浅いコピーで、同じ net に対する - /// 2 回目の forward が 1 回目の出力を黙って書き換えていた。修正は - /// .clone() を足すことだったが、それを実物のモデルで確かめたのは - /// ここが最初である。 + /// 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 + /// を持たない別モデルで検証)でもこのテストを走らせたが、**どちらも + /// このテストは緑のままだった** —— 外した状態と戻した状態とで結果が + /// 変わらない。*output_mat = result;(cv::Mat の代入は shallow だが + /// 参照カウントを共有する)によって mat_table 側の Slot が result の + /// データを生かし続けるため、2 回目の forward が同じ内部バッファへ + /// cv::Mat::create() で書こうとしても参照が生きている間は + /// 再割り当てが起きる、という経路を疑っているが未確認である。 + /// **したがってこのテストは、想定した defect を実際に再現させて + /// 潰したことの証明にはなっていない** —— 契約どおりの結果を実物の + /// モデルで確認した positive な回帰検査ではあるが、`.clone()` を + /// 消しても今のところこのテストは検知できない。同じ形の負の対照が + /// 取れないことがある、という M6 の教訓(CLAUDE.md + /// 「prove-a-check-works」の節)をここでも踏んだ。 + /// /// [Fact] - public void CallingForwardTwiceDoesNotRewriteTheFirstOutput() + public void CallingForwardTwiceWithDifferentInputsDoesNotRewriteTheFirstOutput() { 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 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); - 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, firstOutput); + 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); - var beforeSecondForward = ReadAll(firstOutput); + CvDnn.Forward(net, blobA, firstOutput); + var afterFirstForward = ReadAll(firstOutput); // **同じ net で 2 回目の forward を呼ぶ。** 別の handle // (secondOutput) に書かせるが、defect が再発していれば // net が内部で持つバッファ経由で firstOutput まで書き換わる。 - CvDnn.Forward(net, blob, secondOutput); - + CvDnn.Forward(net, blobB, secondOutput); var afterSecondForward = ReadAll(firstOutput); - Assert.Equal(beforeSecondForward, afterSecondForward); + Assert.Equal(afterFirstForward, afterSecondForward); + + // **前提の健全性チェック。** srcA と srcB が実際に異なる結果を + // 生んでいなければ、上の Assert.Equal は defect の有無に関わらず + // 常に真になり、何も検証したことにならない。 + var secondOutputBytes = ReadAll(secondOutput); + Assert.NotEqual(afterFirstForward, secondOutputBytes); } /// 32FC1(4 byte/pixel、1 channel)の Mat を丸ごと byte[] に読み出す。 @@ -148,4 +177,14 @@ private static byte[] ReadAll(CvMat mat) 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); + } } From feb93d055cca4911bd319149abd4d8eab938a8f6 Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 15:11:10 +0900 Subject: [PATCH 16/21] =?UTF-8?q?docs(m7c):=20Task=204=20=E3=83=AC?= =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=E3=81=AE=E6=8C=81=E3=81=A1=E8=B6=8A?= =?UTF-8?q?=E3=81=97=202=20=E4=BB=B6=E3=82=92=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Runtime/Core/AssemblyInfo.cs: CvUnity.Dnn への InternalsVisibleTo は、 Runtime/Interop/AssemblyInfo.cs が明示的に拒んだ「profile ごとに InternalsVisibleTo を並べる」形をここでやっている。正当化できるのは CvMat.Handle が LibraryName(文字列定数、生成器の複製で足りた)と違い 複製しようのない生きた値だからで、その理由は Interop 側のコメントには 無い。将来 profile が増えたときに、この前例を根拠に Interop 側で InternalsVisibleTo を並べ始めないよう、ここに注記した。 - Runtime/Dnn/CvUnity.Dnn.asmdef: references の CvUnity.Interop を外した。 CvDnn.cs が触れるのは CvUnity(Core、CvMat / CvStatus / CvNative / CvNativeException)と CvUnity.Interop.Dnn(NativeMethodsDnn)だけで、 CvUnity.Interop の型は明示にも暗黙にも一切現れない —— brief にそのまま 残っていた参照で、削っても変わらないことをソースの grep で確認した。 dotnet test --filter "Category!=Probe": 195/195、exit 0(変化なし)。 --- .../Runtime/Core/AssemblyInfo.cs | 17 +++++++++++++++++ .../Runtime/Dnn/CvUnity.Dnn.asmdef | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs b/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs index bbf96c9..312ee36 100644 --- a/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Core/AssemblyInfo.cs @@ -10,4 +10,21 @@ // // 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/Dnn/CvUnity.Dnn.asmdef b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef index 668624e..0707586 100644 --- a/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef +++ b/Packages/com.ayutaz.opencv-unity-native/Runtime/Dnn/CvUnity.Dnn.asmdef @@ -2,7 +2,6 @@ "name": "CvUnity.Dnn", "rootNamespace": "CvUnity.Dnn", "references": [ - "CvUnity.Interop", "CvUnity.Interop.Dnn", "CvUnity.Core" ], From 432207f7a53a846956a57a5d22ca492b20b173ce Mon Sep 17 00:00:00 2001 From: yousan Date: Wed, 9 Sep 2026 15:29:50 +0900 Subject: [PATCH 17/21] =?UTF-8?q?fix(m7c):=20=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=202=20=E4=BB=B6=20=E2=80=94=20?= =?UTF-8?q?=E9=99=B3=E8=85=90=E5=8C=96=E3=81=97=E3=81=9F=E8=A8=98=E8=BF=B0?= =?UTF-8?q?=E3=81=A8=20refcount=20=E8=AA=AC=E3=81=AE=E8=AA=A4=E3=82=8A?= =?UTF-8?q?=E3=82=92=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Important 1: docs/api-reference.md が自分の diff で嘘になっていた。** :485-490 と :985-988(重複記載)の 2 箇所が「正常系はまだ実証していない …後続のタスクが L3 に足す」のままだった。後続のタスクは自分自身で、 実際に足した。DnnInferenceTests.cs(tiny.onnx を使う)が読み込み → blob 化 → forward の往復を実証していることを書いた。**過大に書かない** —— 実証しているのは境界の往復であって、.clone() の効果(アリアシング defect が直っていること)そのものではない、とも明記した。 この種の誤りは「新しい public member が spec/api-reference に無い」という 機械チェックとは別物である —— そちらは通っていた(`docs/api-reference.md documents every spec function`)。ここで壊れていたのは事実の陳腐化で、 どの check も見ていない。「機械チェックが通る」と「文書が正しい」は 別の問いで、この修正は後者にだけ答えている。 **Important 2: テストの docstring が、examination で否定された説を 「未確認だが生きている」かのように書いていた。** `cv::Mat::create()` は要求された shape / type が現在のヘッダと一致すれば 早期リターンする経路を持ち、**そこは参照カウントを一切見ない。** つまり mat_table 側の Slot が result への参照を生かしていることは、2 回目の forward による in-place の上書きを止める理由にならない —— refcount 説は ほぼ確実に誤りだった。 主説を「OpenCV 5 の新しい dnn engine が forward() のたびに新しく確保した バッファを返している」に差し替えた(これも未確認 —— 復元済みツリーには header しかなく OpenCV 本体のソースを読める状態ではない。ただし機構として 否定された refcount 説と対等には扱わない)。 **これは .clone() の位置づけそのものを変える。** 「参照カウントが本来 守ってくれるところの念のため」ではなく、**forward() が返す Mat が net 側の バッファを指していないという保証はどこにも無く、.clone() こそが「返された handle が独立したメモリを持つ」という契約を成立させている唯一のもの**、 という書き方に直した。native/src/ocvu_dnn.cpp 側のコメントも、後続タスク 待ちだった一文(Task 5 が実際に検証を試みて再現しなかった)を合わせて 更新した。 dev.ps1 test-managed: 195/195 + 116/116、exit 0。 dev.ps1 test-tools: 全 PASS(`docs/api-reference.md documents every spec function` を含む)。文書内リンク検査(ci-lint.yml の該当 step をローカルで 実行): 201 件中 0 件破損。 --- docs/api-reference.md | 33 +++++++++++++------ native/src/ocvu_dnn.cpp | 12 +++++-- .../DnnInferenceTests.cs | 33 ++++++++++++++++--- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index dc85400..d925e82 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -482,12 +482,18 @@ width / height の範囲 → src の handle → dst の handle で固定して ならない —— buffer の長さではなく、native がその寸法でメモリを確保する 引数だからである(`ocvu_orb_detect` の `max_features` などと同じ形の上限)。 -**有効な ONNX を読み込ませて推論する経路は、ここではまだ実証していない。** -壊れた入力に対する振る舞い(NULL、負または 0 の長さ、無効な handle、 -異常な blob の寸法)は L1(`native/tests/test_dnn.cpp`)と、公開 API の -壊れた入力・寿命・所有権(`tests/Managed/CvUnity.Tests.Managed/DnnTests.cs`。 -§2.17)が固定している。実物の小さなモデルを使った正常系は、後続のタスクが -L3 に足す。 +**有効な 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 に含まれないもの @@ -982,10 +988,17 @@ C# 側はその戻り値を見ない)。 | `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 モデルを読んで実際に推論する経路)は、この commit ではまだ -実証していない。** `DnnTests.cs`(`tests/Managed/CvUnity.Tests.Managed/`)が見るのは -壊れた入力・寿命・所有権だけで、有効な ONNX を手で組むのは現実的でないため -正常系は別のタスクが実物の小さなモデルを使って L3 に足す。 +**正常系(実物の 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. 対象外(この文書に書かないもの) diff --git a/native/src/ocvu_dnn.cpp b/native/src/ocvu_dnn.cpp index 1f3942e..e4bfa4d 100644 --- a/native/src/ocvu_dnn.cpp +++ b/native/src/ocvu_dnn.cpp @@ -177,8 +177,16 @@ extern "C" ocvu_status ocvu_dnn_net_forward(ocvu_net_handle net, ocvu_mat_handle // 書き換わる —— この ABI の他のすべての関数が新しく確保した // メモリを返しているのに、ここだけが例外になってしまう。 // **未確認事項**: この所有権の挙動は OpenCV のヘッダには明記が - // 無く、実装から推測している。実物の ONNX モデルで forward を - // 2 回呼んで検証するのは、まだ無いので後続タスクの担当である。 + // 無く、実装から推測している。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()); diff --git a/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs index 19e6265..b906c3f 100644 --- a/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs +++ b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs @@ -117,11 +117,34 @@ public void DisposingACvNetTwiceIsSafe() /// このネットワーク(Identity 1 ノード)でも 2 層の Relu(learned weight /// を持たない別モデルで検証)でもこのテストを走らせたが、**どちらも /// このテストは緑のままだった** —— 外した状態と戻した状態とで結果が - /// 変わらない。*output_mat = result;(cv::Mat の代入は shallow だが - /// 参照カウントを共有する)によって mat_table 側の Slot が result の - /// データを生かし続けるため、2 回目の forward が同じ内部バッファへ - /// cv::Mat::create() で書こうとしても参照が生きている間は - /// 再割り当てが起きる、という経路を疑っているが未確認である。 + /// 変わらない。 + /// + /// + /// 「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()` を From caf6a5965f6b34cc1656d335b56681cf581b2a0c Mon Sep 17 00:00:00 2001 From: yousan Date: Thu, 10 Sep 2026 11:42:44 +0900 Subject: [PATCH 18/21] =?UTF-8?q?fix(m7c):=20dnn=20=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=A7=E8=B6=8A=E3=81=88=E3=81=9F=20package=20=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E3=82=92=E7=9B=B4=E3=81=97=E3=80=81M7=20=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=201=20=E3=82=92=E5=88=A4=E5=AE=9A=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **上限超過の原因は dnn ではなかった。** PR #72 の「Assemble the release assets」が実際に落ちた(124,101,923 > 104,857,600)。ELF セクションを直接 読んで測ると、Android の .so は 86% が DWARF デバッグ情報で、dnn を足す前 (v0.3.0)の同じ .so でも既に 82% だった —— dnn は閾値を越えさせただけで、 原因は前から在った。native/CMakeLists.txt に Android 限定の `llvm-strip --strip-unneeded` を POST_BUILD で足して直した(toolchain file ではなくこちらに置いた理由: 構成ハッシュは tools/opencv-config.psd1 の 内容だけを見るため、toolchain file への変更は復元済み OpenCV artifact に 反映されない)。strip 前後で ocvu_ の動的エクスポート 57 本は一致し、16 KB page size の p_align も変わらないことを実測済み。 **上限は上げていない。tarball も分けていない。** ローカルで実際にこの run の 6 platform artifact を材料に assemble/pack/measure を再現し、 124,102,343 → 77,528,652 バイト(上限に対して約 27 MB の余裕)を確認した。 「全部入り tarball を 1 つの artifact のままにするか」という roadmap の 未決事項も、この実測で決定に変えた。 **M7 完了条件 1(profile ごとの native artifact / manifest / notices)を docs/roadmap.md の新しい `### M7c の判定` で判定した。条件 5 には触れて いない**(`### CUDA / cuDNN 同梱の判定` で既に閉じている)。実証していない ことも明記した: 実機で dnn を動かしていない、Web の動作は CI の browser E2E に任せている、推論速度を測っていない。加えて未解決の第三者依存の穴 (MLAS/ONNX Runtime が allowlist から見えない、WITH_CAROTENE/WITH_KLEIDICV が既定 ON のまま未監査、third-party ライセンス集合が platform ごとに違う のに notices が反映していない)と、`PNG_ARM_NEON=off` が ARM 加速を犠牲に した取引であることを記録した。 CLAUDE.md にも dnn profile の在り処(bindings/spec/dnn.json、 Runtime/Dnn/、native/src/ocvu_dnn*.cpp)とリンク済み module 9 個への 更新を反映し、README.md / README.ja.md に `OCVU_PROFILE_DNN` を Player Settings の Scripting Define Symbols で立てる手順を書いた (package.json の versionDefines では表現できない理由も明記)。 --- .github/workflows/release.yml | 18 ++++-- CLAUDE.md | 21 ++++--- README.ja.md | 19 +++++++ README.md | 23 ++++++++ docs/roadmap.md | 102 +++++++++++++++++++++++++++------- native/CMakeLists.txt | 40 +++++++++++++ tools/pack-upm-tarball.ps1 | 7 ++- 7 files changed, 196 insertions(+), 34 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a07d8fd..2ee2f11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -431,10 +431,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..be1a5e4 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` の範囲である)。 @@ -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/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}.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 別化) | +| `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,7 +155,8 @@ 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` には置かない | @@ -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/README.ja.md b/README.ja.md index c19ea0a..7c8a299 100644 --- a/README.ja.md +++ b/README.ja.md @@ -116,6 +116,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..ec3bd2a 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,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/docs/roadmap.md b/docs/roadmap.md index 1dfd724..f2686bc 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2145,11 +2145,11 @@ commit のメタデータ以外に、後から確かめる手立ては無い。 - **`OCVU_ABI_VERSION` は単一の整数のまま**である(正本は `docs/abi-ownership-and-versioning.md` §2 の「決定」。ここで再オープンしない)。 module 分離がこの決定に影響しうるなら、**正本のほうに留保を書く** -- dnn を**別 package**(`…-dnn`)で配るか、同じ package の optional profile にするか。 - **全部入り tarball を配る正にしたのは M3.5 の決着**なので、dnn を足すことは - **「中身を足す」ではなく「形を変える」**ことになりうる。**2026-09-08 に、隣接する - 問い(native binary を profile ごとに分けるか)だけ決まった —— 分けない、と決めた。 - この問いそのものはまだ決めていない。** 詳細と選択肢は下の「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) @@ -2163,15 +2163,42 @@ C# 側が持つ。** 好みではなく構造上の理由による —— `Packa 記録しておく** —— dnn のコードは、profile を一度も有効にしない利用者にも配られる ことになる。 -**まだ決めていない: 全部入り tarball が 1 つの artifact のままかどうか。** 誰も -測っていない数 —— `dnn` と protobuf を積んだときの platform あたりの増分 —— -に懸かっている。全部入りは現在 **69,565,901 バイト(66 MB。6 platform、v0.3.0 の -公開済み asset、2026-09-06 実測 —— 上の「決定: native bridge を module 単位に -分ける」5 に同じ実測がある)**で、`.github/workflows/release.yml` は必須チェックの -中で **100 MB** の上限を課しており、残りはおよそ **34 MB**(6 platform で割ると -1 platform あたり約 5.6 MB。`docs/superpowers/plans/2026-09-05-m7c-dnn-profile.md` -の見直し表にある試算)。**この増分を測るまで決定は据え置く。** 測ったときに比較する -選択肢は 3 つ: +**決定(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` の行)を弱めることになる @@ -2184,6 +2211,21 @@ C# 側が持つ。** 好みではなく構造上の理由による —— `Packa 未解決のまま戻ってくる**(`### 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 利用者には推論エンジンの代替がある**という理由も変わらない。**変わったのは @@ -2202,7 +2244,7 @@ C# 側が持つ。** 好みではなく構造上の理由による —— `Packa ### 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) が正本で、ここには写さない。 @@ -2216,7 +2258,7 @@ C# 側が持つ。** 好みではなく構造上の理由による —— `Packa - **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 件を扱う**) @@ -2233,11 +2275,11 @@ C# 側が持つ。** 好みではなく構造上の理由による —— `Packa - **非既定 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 に決定を書き加えた、文書のみの変更である。 | # | 完了条件 | 判定 | | --- | --- | --- | @@ -2247,9 +2289,31 @@ C# 側が持つ。** 好みではなく構造上の理由による —— `Packa - **この決定が縛るのは「同梱」だけである。** 利用者が自分の環境へ 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 で上流が直せば見直す価値がある。 + --- ## 担当が無かった制約(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/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 ) From 73a0b600123a29eedffa038f40eb3abdab96a566 Mon Sep 17 00:00:00 2001 From: yousan Date: Thu, 10 Sep 2026 13:20:54 +0900 Subject: [PATCH 19/21] =?UTF-8?q?fix(m7c):=20CLAUDE.md=20=E3=81=AE=20modul?= =?UTF-8?q?e=20=E4=B8=80=E8=A6=A7=E3=80=81=E8=A6=8B=E3=81=88=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F2=E7=AE=87?= =?UTF-8?q?=E6=89=80=E7=9B=AE=E3=83=BB3=E7=AE=87=E6=89=80=E7=9B=AE?= =?UTF-8?q?=E3=81=AB=20dnn=20=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit レビュー指摘(Minor)。native/include/opencv_unity_native.h の説明のすぐ下、 生成ヘッダの brace-list と cmake/FindOpenCvUnityDeps.cmake の COMPONENTS の 説明が dnn を欠いたままだった——1 つ上の段落(本文中の同じ brace-list)は 先に直っていたのに、同じ commit の中で 2 箇所目が漏れていた。 check-platform-list-drift.sh が「見えないのは同一ファイル内の 2 つ目以降の 一覧」と記録している、まさにその形。3 箇所目が無いことは brace-list の 形で grep して確かめた。 --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index be1a5e4..e36dd9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,12 +115,12 @@ 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` / `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_*` 定数を持ち、下の 10 個を 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/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 本)**、**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 側が何をビルドするかであって、こちらが何をリンクするかではない | +| `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 を判定する | From 6268c949e6581d3f9b4908317611a82dd7a75273 Mon Sep 17 00:00:00 2001 From: yousan Date: Thu, 10 Sep 2026 14:39:07 +0900 Subject: [PATCH 20/21] =?UTF-8?q?fix(m7c):=20=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC2=E5=91=A8=E7=9B=AE=20=E2=80=94=20Important=206?= =?UTF-8?q?=E4=BB=B6=E3=83=BBMinor=204=E4=BB=B6=E3=82=92=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **I1(最重要): dnn の到達性テストがどこからも呼ばれていなかった。** AbiReachabilityChecksDnn.CallEveryEntryPoint() は生成されていたが呼び出し元が 0箇所で、呼ばれない P/Invoke 宣言は IL2CPP の stripping が消せる——検査が 在ることが何の保護にもなっていなかった。PlayMode に #if OCVU_PROFILE_DNN で 守った呼び出し元を追加し、手動で1回、define を立てて test-unity-player を 実行して測定した: 40 passed、EveryDnnEntryPointIsReachable が実際に走り dnn の4宣言が消えていないことを確認した。CI はこの define を立てないので、 自動再検証はできないことを roadmap の5つ目の穴として明記した。 **I2**: CLAUDE.md の古い件数(20/215/185)を再実測して直した(23/234/196)。 **I3**: Task 5 が既に閉じた作業を「後続タスクの担当」のまま残していた 3箇所を直し、.clone() の負の対照が取れなかった経緯を正しく書いた。 **I4**: ocvu_net_handle にも §1.5 の複数スレッド規約が適用されることを明記。 **I6**: PNG の ARM 加速喪失(Android/iOS/macOS、dnn とは無関係)を README.md / README.ja.md に追記。 **I5**: ci-native.yml と release.yml で食い違っていた Android skip の コメントを、検証せず両方とも防御的な表現に揃えた。 **M6 は提案どおりには閉じなかった。** width/height を8にする案は実測すると 例外を投げなかった(tiny.onnx は Identity 1 ノードで、OpenCV が実行時の 入力から shape を再推論するため)。代わりに BlobFromImage を経由しない 生の CV_8UC3 Mat を渡す形で、実物の cv::Exception (incompatible type of input tensor: CV_8UC3 given, CV_32FC1 expected)を 実際に踏むテストに書き直した。 Minor: M1(ProfileGatingTests の前提 assert 追加)、M2(PNG_ARM_NEON の 「仮説」を実測済みの事実に)、M3(CLAUDE.md の api-reference 行に CvDnn/CvNet を追加)。 test-unity-player 実行後に立てた OCVU_PROFILE_DNN の define は ProjectSettings.asset から復元し、git diff / sha256sum で HEAD と 完全一致することを確認した。 --- .github/workflows/ci-native.yml | 17 ++++--- .github/workflows/release.yml | 9 +++- CLAUDE.md | 8 ++-- README.ja.md | 2 + README.md | 7 +++ docs/abi-ownership-and-versioning.md | 41 +++++++++++++++-- docs/roadmap.md | 1 + .../DnnInferenceTests.cs | 46 +++++++++++++++++++ .../Managed/CvUnity.Tests.Managed/DnnTests.cs | 3 +- .../Tests/EditMode/ProfileGatingTests.cs | 8 ++++ .../PlayMode/AbiSurfaceDnnPlayerTests.cs | 46 +++++++++++++++++++ .../PlayMode/AbiSurfaceDnnPlayerTests.cs.meta | 2 + .../PlayMode/CvUnity.Tests.PlayMode.asmdef | 3 +- .../Assets/Tests/Shared.Dnn/README.md | 20 ++++++-- tools/opencv-config.psd1 | 10 ++-- 15 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs create mode 100644 tests/UnityProject/Assets/Tests/PlayMode/AbiSurfaceDnnPlayerTests.cs.meta 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 2ee2f11..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 diff --git a/CLAUDE.md b/CLAUDE.md index e36dd9c..94dda0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,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 件になったが所要時間は取り直していない) | @@ -162,7 +162,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | `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 の追跡外にあるため) | diff --git a/README.ja.md b/README.ja.md index 7c8a299..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 以降が必要です。 ## 導入 diff --git a/README.md b/README.md index ec3bd2a..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 diff --git a/docs/abi-ownership-and-versioning.md b/docs/abi-ownership-and-versioning.md index dd10b8b..c126bf5 100644 --- a/docs/abi-ownership-and-versioning.md +++ b/docs/abi-ownership-and-versioning.md @@ -274,6 +274,18 @@ native はそこへ書くだけで、戻った後は一切保持しない。 生かし続ける。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 を使っている側 @@ -830,8 +842,25 @@ Important 2)。** `cv::dnn::Net::forward()` が返す `Mat` はネットワー 指したままになり、同じ net で 2 回目の forward を呼ぶと 1 回目の出力が 黙って書き換わる —— この ABI の他のすべての関数が新しく確保したメモリを 返す契約と矛盾する。**この所有権の挙動は OpenCV のヘッダに明記が無く、 -実装から推測している。** 実物の ONNX モデルで forward を 2 回呼んで -確かめるのは、まだ無いので後続タスクの担当である。 +実装から推測している。** + +**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 @@ -1044,8 +1073,12 @@ platform(iOS / WebGL)で `"__Internal"` にならず、Player の中で最 - L3(`DnnTests.cs`)が壊れた入力・二重解放・null/空 byte 列を固定した (185 → 189、+4) -**まだ実証されていないこと**(Task 4 の範囲外)は、上の「有効な ONNX を -読み込んで実際に推論すること」だけが残る——**Task 5 の担当である。** +**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/roadmap.md b/docs/roadmap.md index f2686bc..6549e3f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2313,6 +2313,7 @@ CI 自身での確認は `.superpowers/sdd/2026-09-05-m7c-dnn-profile/task-6-rep - **`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 が変わったときに自動では再検証されない。** --- diff --git a/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs index b906c3f..7aac6ea 100644 --- a/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs +++ b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs @@ -191,6 +191,52 @@ public void CallingForwardTwiceWithDifferentInputsDoesNotRewriteTheFirstOutput() 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` として返ってくる(実測のメッセージを + /// そのまま確認する)。**呼ぶ側が `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) { diff --git a/tests/Managed/CvUnity.Tests.Managed/DnnTests.cs b/tests/Managed/CvUnity.Tests.Managed/DnnTests.cs index 2cc17a3..9d23b59 100644 --- a/tests/Managed/CvUnity.Tests.Managed/DnnTests.cs +++ b/tests/Managed/CvUnity.Tests.Managed/DnnTests.cs @@ -6,7 +6,8 @@ /// /// **有効な ONNX を手で組むのは現実的でない。** /// ここが見るのは、壊れた入力・寿命・所有権である。 -/// **正常系は Task 5 が、実物の小さなモデルを使って見る。** +/// **正常系は が、実物の小さなモデル +/// (tiny.onnx)を使って見ている**(Task 5 で追加済み)。 /// public class DnnTests { diff --git a/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs b/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs index 1230796..40bc71a 100644 --- a/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs +++ b/tests/UnityProject/Assets/Tests/EditMode/ProfileGatingTests.cs @@ -143,6 +143,14 @@ public void TheDnnSharedTestAssemblyIsAbsentWithoutItsDefine() [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), 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/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 503450a..32eb710 100644 --- a/tools/opencv-config.psd1 +++ b/tools/opencv-config.psd1 @@ -156,9 +156,13 @@ `-DPNG_ARM_NEON=off` にすると、この `enable_language(ASM)` が 呼ばれなくなり(`arm/arm_init.c` 等の intrinsics 実装も含めて NEON 最適化そのものが丸ごと無効になる)、mlas の判定が - x86_64 と同じ「plain ASM が無い」経路を取れるようになるはず、 - という仮説にもとづく。**代償は明確: PNG の NEON デコード - 高速化を、Android / macOS / iOS の 3 platform で失う。** + 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 だけに絞った変更である。 From f05522d102ef8c6290e8ea75a0720186a7cfd85f Mon Sep 17 00:00:00 2001 From: yousan Date: Thu, 10 Sep 2026 14:49:19 +0900 Subject: [PATCH 21/21] =?UTF-8?q?fix(m7c):=20docstring=20=E3=81=AE?= =?UTF-8?q?=E9=87=8D=E8=A4=87=20=20=E3=82=BF=E3=82=B0=E3=81=A8=20?= =?UTF-8?q?assert=20=E3=81=AE=E8=AA=AC=E6=98=8E=E3=82=92=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DnnInferenceTests.cs の一番丁寧に書いた docstring(.clone() を巡る 負の対照の顛末を記録した場所)に、コピペ由来の重複 開始タグが 残っていた。ビルドには影響しない(GenerateDocumentationFile も TreatWarningsAsErrors も無い)が、直す価値のある場所だからそのままにしない。 ついでに docstring 内の「メッセージをそのまま確認する」という表現も、 実際の assert(Assert.Contains による CV_8UC3 / CV_32FC1 の部分一致、 完全一致ではない)と一致する言い方に直した——完全一致にしなかったのは OpenCV の点リリースが文言を書き換えても検査が生き残るための意図的な 選択で、それを正しく記録しておく。 --- .../Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs index 7aac6ea..13d25ff 100644 --- a/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs +++ b/tests/Managed/CvUnity.Tests.Managed/DnnInferenceTests.cs @@ -191,7 +191,6 @@ public void CallingForwardTwiceWithDifferentInputsDoesNotRewriteTheFirstOutput() Assert.NotEqual(afterFirstForward, secondOutputBytes); } - /// /// /// **`ocvu_dnn_net_forward` の `catch (const cv::Exception&)` 経路 /// (`native/src/ocvu_dnn.cpp` の forward 実装、`OCVU_STATUS_OPENCV_ERROR` @@ -217,9 +216,12 @@ public void CallingForwardTwiceWithDifferentInputsDoesNotRewriteTheFirstOutput() /// に渡すと、`net.setInput()` が /// incompatible type of input tensor #0: CV_8UC3 given, CV_32FC1 /// expected という `cv::Exception` を実際に投げ、 - /// `OCVU_STATUS_OPENCV_ERROR` として返ってくる(実測のメッセージを - /// そのまま確認する)。**呼ぶ側が `BlobFromImage` を通さずに `Forward` - /// を呼ぶ**という、空間サイズの取り違えより現実的な誤用でもある。 + /// `OCVU_STATUS_OPENCV_ERROR` として返ってくる。**assert は完全一致では + /// なく `CV_8UC3` / `CV_32FC1` の部分一致(Assert.Contains)に + /// している** —— OpenCV の点リリースが文言を書き換えても、型名が + /// 変わらない限り検査が生き残るようにするための意図的な選択である。 + /// **呼ぶ側が `BlobFromImage` を通さずに `Forward` を呼ぶ**という、 + /// 空間サイズの取り違えより現実的な誤用でもある。 /// /// [Fact]