Skip to content

feat(m7c): dnn を opt-in profile として足す(作業中) - #72

Merged
ayutaz merged 21 commits into
mainfrom
feat/m7c-dnn-profile
Sep 10, 2026
Merged

ayutaz merged 21 commits into
mainfrom
feat/m7c-dnn-profile

Conversation

@ayutaz

@ayutaz ayutaz commented Sep 9, 2026

Copy link
Copy Markdown
Owner

作業中の PR である。 M7c の 6 タスクのうち 3 つ(OpenCV の dnn ビルド / ocvu_net_handle の表 / C ABI 4 本)が済んだ段階で開けた。merge しない。

開けた理由は測定である。 決着を先送りしてある決定が 1 つ残っており
(docs/roadmap.md の「dnn の配布の形」)、それは 全部入り tarball が 1 つのままでいられるかである。
判断材料は「dnn を入れた plugin が platform ごとにどれだけ大きくなるか」で、
手元では Windows しか測れない。 release.yml は PR で空撃ちされ、
全部入りを組み立てて measure-package-size.ps1 に掛けるので、この PR がその測定装置になる。

手元での実測(Windows、debug)

33,565,184 → 34,873,856 バイト、増分 +1,308,672(約 1.25 MB)。

恐れていたより 2 桁小さい。 リンクされうる材料は opencv_dnn + libprotobuf で
Windows だけで約 105 MB あり、全部入りの余裕は 1 platform あたり約 5.6 MB だった。
デッドコード除去が効いたということだが、これは debug ビルドの 1 platform 分でしかない。

ここまでで成立したこと

  • OpenCV が dnn 込みで 6 platform ビルドできる(3 回かかった。1 回目は WITH_PROTOBUF=OFF との非両立、2 回目は依存 allowlist が protobuf を正しく拒否、3 回目 green)
  • protobuf と flatbuffers を allowlist と notices に正しく入れた —— ライセンスは一次情報で確認し全文を再現
  • ocvu_net_handle の表(M3 の use-after-free の形を 2 つ目の表で再生産しないため、C ABI より先に固定した)
  • C ABI 4 本と bindings/spec/dnn.json —— profile 機構が本物の spec で初めて動いた

途中で見つかったもの

  • 失効した前提が 3 箇所(WITH_PROTOBUF=OFF / allowlist の denylist / notices の dlpack・flatbuffers 分類)。いずれも消さずに書き換え、理由を残した
  • 元の dlpack / flatbuffers 分類は欠陥のある方法で行われていた —— :: を含む検索パターンは MSVC のマングルに当たらない。補正して再検索し、flatbuffers は実際にリンクされていた(Windows と Linux の 2 つのマングル方式で確認。残る 4 platform は推論であって未検査)
  • protobuf の名前は libprotobuf —— Unix では lib が二重に付いて liblibprotobuf.a になる

残り

Task 4(C# の公開 API)/ Task 5(実物のモデルで正常系)/ Task 6(配布と判定)。
M7 の完了条件 1(profile ごとの artifact / manifest / notices)は Task 6 が閉じる。

Comment thread tests/Managed/CvUnity.Tests.Managed/AllocationTests.cs Fixed
ayutaz added a commit that referenced this pull request Sep 9, 2026
macOS arm64 で opencv_unity_native.dylib のリンクが
「MlasHGemmSupported が未解決」で落ちていた(PR #72、run 34305540718)。
根本原因は上流 OpenCV 5.0.0 の vendoring 欠陥(3rdparty/mlas/lib/compute.cpp
の明示的テンプレート特殊化 MlasGQASupported<MLAS_FP16> が、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 の実行結果を見て確認する。
Comment on lines +22 to +23
var path = Path.Combine(
AppContext.BaseDirectory, "TestModels", "tiny.onnx");
ayutaz added a commit that referenced this pull request Sep 10, 2026
**上限超過の原因は 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 では表現できない理由も明記)。
「5.0 に固定した DNN ラッパーを作り込まない」は 2026-09-08、リポジトリ所有者の
依頼により解除した。module 分離(M7b、2026-09-06)は済んでおり、上流の作り直し
リスクを提示したうえで所有者が着手を依頼した。5.1 の根拠自体は否定されていない。

あわせて、dnn の配布の形について決まったこと(native binary は profile ごとに
分けない。iOS / WebGL の __Internal 静的リンクには切り替える対象が無いため)と、
まだ決めていないこと(全部入り tarball を 1 つの artifact に保つか)を記録した。
レビューの Important 指摘を受け、所有者の依頼を要約ではなく原文のまま
引用する(何を決めるべきか尋ねた発言と、作り直しリスクを提示された
うえでの「進めてください」の2つ、順序込みで)。あわせて、この経緯の
記録がこの commit にしか無く、issue・ticket・外部記録が無いことを
文書自身の声で明記する。
**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 と同じ、再現性のための判断)。
**依存 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 に差し替えた。
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 も持たない)が、個別に数えて初めて見えた。
PackageRelease.Tests.ps1 の「the tracked notices do not hardcode a
configuration hash」が実際に落ちて見つかった —— flatbuffers / dlpack の
節に c21fe00b8952 を 2 箇所書いてしまっていた。CLAUDE.md 自身が M3 で
19 箇所死んだ例として警告している形と同じ間違いである。run ID
(34215362804)だけを残し、ハッシュは削った。
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> の破棄に 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 で無影響。
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 つは正反対で
(検査が静かに緑のまま無意味になることこそ最も恐れる壊れ方で、赤くなる
のはその壊れ方が起きていないことの証拠である)、コメントを実際に起きた
方へ書き換えた。
**(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<cv::dnn::Net> の破棄に必要なため)。
それでも 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 一覧が食い違う」
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、ほぼ相殺)。
macOS arm64 で opencv_unity_native.dylib のリンクが
「MlasHGemmSupported が未解決」で落ちていた(PR #72、run 34305540718)。
根本原因は上流 OpenCV 5.0.0 の vendoring 欠陥(3rdparty/mlas/lib/compute.cpp
の明示的テンプレート特殊化 MlasGQASupported<MLAS_FP16> が、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 の実行結果を見て確認する。
**公開 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 で更新した。
**モデルは外から取ってこず、自分で作った** —— 外部のモデルは
ライセンスの確認が要り、リポジトリに数 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。
**最初の実装は同じ 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(変化なし、ロジックの
修正のみ)。
- 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(変化なし)。
**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 件破損。
**上限超過の原因は 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 では表現できない理由も明記)。
@ayutaz
ayutaz force-pushed the feat/m7c-dnn-profile branch from 69ab734 to caf6a59 Compare September 10, 2026 04:12
レビュー指摘(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 して確かめた。
**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 と
完全一致することを確認した。
DnnInferenceTests.cs の一番丁寧に書いた docstring(.clone() を巡る
負の対照の顛末を記録した場所)に、コピペ由来の重複 <summary> 開始タグが
残っていた。ビルドには影響しない(GenerateDocumentationFile も
TreatWarningsAsErrors も無い)が、直す価値のある場所だからそのままにしない。

ついでに docstring 内の「メッセージをそのまま確認する」という表現も、
実際の assert(Assert.Contains による CV_8UC3 / CV_32FC1 の部分一致、
完全一致ではない)と一致する言い方に直した——完全一致にしなかったのは
OpenCV の点リリースが文言を書き換えても検査が生き残るための意図的な
選択で、それを正しく記録しておく。
@ayutaz
ayutaz merged commit e1b0930 into main Sep 10, 2026
33 checks passed
@ayutaz
ayutaz deleted the feat/m7c-dnn-profile branch September 10, 2026 06:05
ayutaz added a commit that referenced this pull request Sep 10, 2026
設定はすでに変更済みで、この変更はそれに文書を合わせるものである。

`Web wasm32 (cross-build)` / `Web browser E2E` / `Package web-wasm` の 3 本を
必須チェックへ昇格させた(21 → 24 本)。**帰結: Web のレーンは、いまは
赤ければ merge を止める。**

根拠は実測である —— #63 以降に merge された 11 本の PR(#63〜#70、#72〜#74)
すべてで 3 本とも success だった。当初は「直近 5 本」と書いたがレビューで
指摘されて数え直したところ、根拠はもっと強かった(旧記載の並びは「直近」でも
「連続」でもなく、何も指していなかった)。モバイルの 2 本が M4 の直後は必須外で
2026-08-31 に昇格したのと同じ手順である。

設定は required_status_checks だけを PATCH した —— /protection への PUT は
送らなかった項目を消しうる。変更後に strict / enforce_admins / 直線履歴 /
force push 禁止 / 削除禁止 / レビュー必須数がいずれも元のままであることを
確認した。

CLAUDE.md は「必須チェックの正本はこの表ではなく GitHub 側の設定である。
増やしたらこの表も一緒に直すこと」と書き、さらに「二重に書かれているのは
両 README の 2 段落だけという状態を保つ」と書いている。その 3 箇所を同じ
commit で直し、CLAUDE.md の列挙 24 件が API の 24 件と過不足なく一致する
ことを機械で突き合わせた。

必須でないものを実測し直したら 6 本ではなく 9 本だった —— CodeQL の集約
check と、PR では skip される Unity の結果公開 job 2 本が漏れていた
(旧記載の「9 本」も同じ穴を持っており、実際は 12 だった)。docs/roadmap.md
の M6 節が「必須にするかはまだ決着していない」のまま残っていたので解消した
項として書き直し、両 README がリンク済み module を 8 つと書いていたのを 9 つ
に直した(dnn が抜けていた)。

AI レビュー 2 回で、こちらが作り込んだ欠陥が 2 件出た。どちらも編集した本文の
中身は正しく、周囲との接続だけが壊れている形である: (1) 既存の閉じ記号の
直前に開き ** を挿したため対応がずれ、足した文が太字にならず生の ** が本文に
出ていた。(2) 必須外を 9 本に直したとき「残る 3 本は…」と書いたが 9 - 5 = 4 で、
読者が数えると 8 本で止まる。レビュアーは「偶数か」ではなく記号の対応そのものを
追っており、そちらの検算のほうが強い —— 行ごとの偶奇は必要条件でしかない。

確かめていないこと: GitHub の実レンダラでの描画は見ていない。Unity の結果公開
2 本が「PR では常に skip」かは 4 本の PR での実測にとどまる。gh pr checks が
返すのは各 check の最終結果なので、「一度も赤くならなかった」ではなく
「最終的に緑だった」である。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants