Repository navigation
インスタントリプレイを実装する(Issue #4) - #286
Open
FlowingSPDG wants to merge 212 commits into
Open
FlowingSPDG wants to merge 212 commits into
FlowingSPDG wants to merge 212 commits into
Conversation
Issue #4 の要件を、セッション/コア/Windowsホスト/macOSホスト/ドキュメントまで通しで実装した。 - セッション: ReplayDto(保存先・画質・区間・バッファ・保持方針・警告しきい値・角度・イベント・ プレイリスト・チャンネル)を追加。リプレイ入力がある間だけ存在し、既定セッションには含めない。 - ミキサ: 角度ごとの区間録画、マーク、チャンネル送出(再生/一時停止/速度/ジョグ/ライブ)、 波形ピーク、MP4書き出し、ヘルス統計を C ABI で公開した。 - トランジション: Overwrap(合計 A+B−D)と Additive(合計 A+D+B)を分離し、挿入中は前後の フレームを保持してディゾルブする。ブレンドしたフレームには固有のタイムスタンプを与え、 コンポジタの重複判定で挿入中の絵が捨てられないようにした。 - Windows: リプレイウィンドウとリプレイ設定ウィンドウ(メインUIのボタンからのみ開く)。 マーク/トランスポートのキーボード操作(I/O/Space/←/→/L)、波形のIN/OUTと録画ヘッド、 イベントとプレイリストの編集、書き出し4モード、拒否時の理由表示。 - macOS: 同じ操作面を SwiftUI で実装し、リモート操作とアニメーションからのキューにも対応した。 - vMix互換API: ReplayStartRecording / ReplayStopRecording / ReplayMarkIn / ReplayMarkOut / ReplayCue / ReplayPlay / ReplayPause / ReplayChangeSpeed / ReplayJumpToNow。 - ドキュメント: インスタントリプレイのページを追加し、関数リファレンスと設定ページを実装に合わせた。 - テスト: 録画・マーク・再生・送出(画素)、遷移(Overwrap/Additive の画素)、音声(入力メーターと プログラムバス)、vMix API、アニメーションキュー、非オンユニット録画を統合テストで追加した。
macOSのアタッチテストが、ミキサが無い場合に ERR_INVALID_ARGUMENT を期待と 違って受け取っていた。ユニットの事前チェックで with_mixer のエラー(未作成)を そのまま返し、ユニットだけが無い場合を不正な引数として扱う。Windows側にも 同じ検査を追加した。
CIのmacosジョブが検出した実際のエラーを修正した。 - EivizMixer(Cモジュール)の import が無く、mixer_capabilities / EIVIZ_SCENE_BASE / EivizReplayStats を解決できていなかった(ReplayView / ReplayModel / MixerController+Replay)。 - ReplayAngleInfo の memberwise 初期化で audioUnitId ラベルが抜けていた。 - リプレイ設定の音声ピッカーが参照する audioSelection が未定義だった(状態を追加し、 選択中シーンのアングルの音声を読み込むようにした)。 - SceneAnimView.replayRow が複数文の関数で return を書いていなかった。
実際に触って分かった問題を直した。 - インスタントリプレイのウィンドウから「リプレイ設定」を開けるようにした(メインUIと同じ 経路を共有し、既に開いていれば前面に出す)。 - 3Play/vMix のインスタントリプレイと同様に、ウィンドウ内で「出力(チャンネルが送出して いる映像)」と「入力(録画中のアングル)」を並べて見られるようにした。 - 保存先の指定をモダンなフォルダ選択(Windows: OpenFolderDialog / macOS: NSOpenPanel)に 対応させ、パス入力は任意にした。ドライブ未選択で削除したときの案内も追加。 - 「操作を受け付けませんでした」が理由不明で出ていた問題を修正。録画は保存先とアングルが 無い場合にその旨を、キュー/保護/削除は対象未選択を、再生はキュー未実施を明示し、 それ以外はミキサー自身の理由を併記する。停止は録画中でなければ何も言わない。 - 一覧(イベント/プレイリスト/ドライブ/アングル)にダークテーマの配色と選択色を与え、 既定の白背景で描画されていたスタイル崩れを直した。トランスポートの各ボタンに ツールチップとローカライズ済みラベル(コマ送り)を与え、録画中/停止中でボタンの 有効・無効を切り替えて状態が見えるようにした。 - macOS 側も同じ導線・モニター・フォルダ選択・メッセージに揃えた(L10n EN/JA 469 キー)。
実際に触って分かった問題を直した。 - メインUIのリプレイ入口を「インスタントリプレイ」1つにした。リプレイ設定はデッキの ヘッダから開く(メインUIに設定ボタンは置かない)。 - マークを録画状態から切り離した。ミキサは録画停止時にストアを閉じていたため、停止後は マークも再生もできなくなっていた。停止は書き込みだけを止め、記録済みの素材は マーク・再生できるままにする(ストアを閉じるのは Replay を外したときと終了時だけ)。 mark_in は「録画中」ではなく「記録済みフレームがあるか」で判定する。 - リプレイ設定の保存先はフォルダ選択(Windows: OpenFolderDialog / macOS: NSOpenPanel)で 選んだ時点で一覧に追加する。パス手入力と「ドライブ追加」の手順は廃止した。 - 適用/キャンセルのフィードバックを明確にした。適用に失敗したら理由をダイアログで出して ウィンドウは閉じない。成功したら閉じてメインウィンドウにトーストで知らせる。状態表示は スクロールしない位置(上部)へ移した。 - デッキのレイアウト崩れを直した。モニターの固定 640x360 が列からはみ出していたので 可変サイズにし、出力/入力は縦積み、下部の操作列は折り返しにした。 - macOS も同じ導線・フォルダ選択・メッセージに揃えた(L10n EN/JA 472 キー)。
動いているアプリに対して `eivizctl` で通しの操作をしたところ、API経由の操作に穴があった。
- セッションAPIで Replay 入力を追加できなかった(`Replay input requires a Replay document`)。
`CreateInput` で kind が Replay のときは `add_channel_input` を通し、チャンネル・予約済み
入力ID・リプレイ設定を同時に作るようにした。ホストの追加操作と同じ結果になる。
- `GetReplayState` が常に空を返していた。`MixerFacade` が `replay_state_json` を上書きして
おらず、トレイト既定の `{}` が返っていたため。ミキサの制御サービスへ転送するようにした。
これで録画状態・チャンネルの位置と速度・ドロップ数がAPIから読める。
- `eivizctl replay configure --from-json` を追加。保存先・アングル・バッファリングを
コマンドラインから設定できる(従来はホストのUIのみ)。
- 再生が実時間で進み、クリップ終端で止まることをテストで実測するようにした
(600msで20〜50フレーム進むこと、終端で playing が落ちてクリップ内に留まること)。
- 録画停止後もマークと再生ができることをテストで実測(停止は書き込みだけを止める)。
- ドキュメント(日英)にネイティブAPIからのリプレイ操作と `replay state` を追記。
UI Automation でホストを操作し、入力追加 → リプレイ設定 → 録画 → マーク → キュー → 再生まで通したところ、テストでは出なかった不具合が出た。 - リプレイ設定の「適用」で例外が出ていた。この窓はデッキから非モーダルで開くため DialogResult を設定できず(InvalidOperationException)、窓が閉じず確認も出なかった。 適用はトースト+Close、キャンセルは Close にした(macOS も同じ挙動に揃え、 適用の成否を返すようにして失敗時は理由を出す)。 - マークアウトでイベントが作られなかった。イベント作成時に `playlist["revision"]` を ulong として読むが、書き込みが int リテラル(Int32 ノード) だったため System.Text.Json が例外を投げていた。同じ型で書く(revision は 1UL、 durationFrames は 0U)。 - プレイリストにクリップ行が出なかった。speed / gainDb を double で書き GetValue<float>() で読んでいたため、行の生成で例外になっていた。double で読む。 - アクティブアングル未設定だとクリップを作れず、モーダルのダイアログでデッキが 止まっていた。ミキサは先頭アングルを既定として扱う(live と jump_to_now)ので、 ホストも同じ既定で読み、案内はモーダルではなく通知行に出す(Windows/macOS 共通)。 - デッキのモニターが 628x142 と低すぎたので、最低 220px・ウィンドウ 1000px にした。 これで UI だけで 入力追加 → 保存先のフォルダ選択(即追加)→ アングル追加 → 適用 → 録画 → マークイン/アウト → イベントとクリップの自動作成 → クリップ選択 → キュー → 再生 が通ることを実機で確認した。
録画開始の失敗はすべて同じ i32 に潰れ、しかも replay の C ABI は
`mixer_last_error` に何も書いていなかったため、ホストには
「操作を受け付けませんでした」しか出せなかった。原因ごとに理由を返す。
- mixer: `ReplayFailure { code, message }` を導入し、`configure` / `gate` /
`record` が具体的な理由を返すようにした。文言は英語で、そのまま操作者が
読める内容にする(enabled でない / 保存先が無い / アングルが無い /
format が未設定 / scene が mixer に無い / 空き容量を取得できない /
空き容量不足 / Pro モジュールが無い / Pro が録画を拒否した など)。
- `free_bytes` の失敗を 0 と混同しない(不明は不明として拒否し、理由を返す)。
- アングルのドライブ割当が無い場合の `unwrap_or(0)`(1本目へ黙ってフォール
バック)を削除し、設定エラーとして拒否する。
- 区画フレーム数・エンコードキュー・書き込みバッファの範囲を mixer 側で
検査する(store が黙って clamp していた分を、理由のある拒否に変える)。
- FFI: `mixer_replay_configure` / `mixer_replay_record` は失敗時に
`report_session_error(&message)` を呼び、理由を `mixer_last_error` に残す。
- control: `ReplayFailure::into_control` で理由を保ったまま `ControlError` に
変換する(`control_error` の「全部同じ文言」を経由しない)。
`ReplayRecord` は mixer に投げる前にセッション文書を見て、欠けている設定を
名指しで拒否する。
- テスト: `replay_record.rs` に「format 未設定は理由付きで拒否し、設定を直せば
そのまま録画できる」「scene が無い場合は scene 名を出す」「ドライブ未割当は
割当なしと出す」を追加。Pro モジュール無しは別バイナリ
(`replay_record_nomodule.rs`) で、理由が generic 文言に潰れないことを確認。
- 既存の replay 統合テストは、ドライブ割当が必須になったことに合わせて
`angleIds` を明示するよう更新(6ファイル)。
アングルは Scene しか指せず、録画方式も Composite しか無かった。Issue #4 の 方式 C(入力の生データをそのまま保存)を入れる土台として、データモデルを広げる。 - `AngleDto` に `source`(Scene | Input)と `input_id` を追加。`AngleRecordMode` に `Passthrough` を追加(再エンコードせずに受信フレームを保存する方式。 OMT 入力のみ)。 - protobuf / JSON / 検証 / ホストの4面を同時更新。`session.proto` の `Angle` に `source` と `input_id`、`AngleRecordMode` に PASSTHROUGH、`AngleSourceKind` を 追加。 - `ReplayWarnings` の3しきい値を proto3 の `optional` にして presence を持たせ、 ロード時に 0 を 600/300/60 へ書き換える処理を削除(明示的な 0 を保存できる)。 - `validate_replay` を強化: - `enabled` のとき format(解像度・fps)と区画フレーム数を必須にし、範囲 (区画 30..=1800)を検査する。ドライブとアングルがあるのに `enabled` が false の文書も拒否する(フラグは独立したスイッチではない)。 - Scene アングル: input_id を持てない・Passthrough 不可・scene の存在・同一 scene の二重登録禁止。 - Input アングル: scene_id を持てない・input の存在・映像を持たない入力 (Audio)不可・Replay チャンネル自身は不可・Passthrough は OMT のみ。 - 全アングルがいずれか1本のドライブに明示割当されていること(未割当はエラー)。 - `ReplayDto::normalize()` を `Document::canonicalize` からも呼び、ファイル読み込み でもカウンタを修復する。`reserve_angle_id()` を追加(アングル追加で id を消費)。 - `SessionMutation::UpdateReplaySettings` を追加。保存先・format・バッファリング・ 保持方針・アングル・自動追加・enabled だけを書き換え、イベントとプレイリストは 触らない。ホストが古いコピーから Apply しても、直前に切ったイベントを消さない。 - quota 検査を「結果の文書」で行うようにし、`CreateInput` / `UpsertInput`(Replay) からも通す(自分で Replay 入力を追加してプラン上限を迂回できていた)。 - `ClipDto.preserve_pitch` の既定を true に(Issue の追加コメント「速度を下げた ときのピッチ下げを防ぐ」)。 - テスト: アングルのソース規則、Passthrough の制約、二重登録、format 必須、 区画フレームス範囲、ドライブ必須、enabled 規則、Mix Unit が無い文書の拒否、 設定のみ更新でイベントが残ること。
データモデルの変更(アングルのソース、ドライブ必須、enabled の規則、設定専用
mutation)にホストが追随していなかったため、UI から設定しても検証で拒否され、
録画を開始できない状態だった。実機で通しの確認をして直した。
- Windows のリプレイ設定:
- ドライブが1本のときは全アングルをその1本に割り当てる(録画先を推測しない)。
- `enabled` は「ドライブが1本以上 かつ アングルが1本以上」。ドライブだけで true に
していたため、アングル無しで有効な文書を作れてしまっていた。
- アングルに `source` / `inputId` / `recordMode` を書き、読み込み時も読む。
表示名は Input アングルなら入力名を使う(`replay.inputMissing` を追加)。
- リモートへの Apply は `updateReplaySettings`(設定のみ)を送る。イベントと
プレイリストはコアが持つものなので、古いコピーで上書きしない。
- Windows のデッキ: Record の前にセッションの最新 JSON を読み直し、`enabled` も
確認する(古い文書で録画を試みて理由の分からない拒否になるのを防ぐ)。
- macOS: 新規リプレイ文書に **録画 format** を持たせる(最初の Mixing Unit から。
これが無いと macOS では永久に録画できない)。`enabled` の規則、1本ドライブへの
割り当て、アングルの `source`/`inputId`/`recordMode`、設定専用 mutation も同じに
揃えた。`ReplayAngleInfo.liveSourceId` を追加(Input アングルのライブ表示用)。
- `ReplayModel.SerializeSettings` / `ReplayDocument.settingsJSON` を追加。
実機(Windows)で確認: 入力追加 → リプレイ設定(フォルダ選択で即追加・アングル追加)
→ 適用 → 録画開始(state=1)→ マークイン/アウトでイベントとクリップ自動作成 →
プレイリストで選択 → キュー → 再生 → 番組のスクリーンショットに録画済みクリップの
映像が出ること、保存した .eivz にリプレイ文書(enabled/保存先/割当/format/イベント/
クリップ)が入っていることを確認した。
実機で「録画 → 停止 → マークイン → マークアウト」を試したところ、長さ0の クリップが出来てしまい、再生しても何も出なかった(フレームが無いので当然)。 イベントとクリップは作られるのに中身が無いという、いちばん分かりにくい壊れ方 だったので、作る前に断るようにした。 - mixer: `close_mark` は `out_frame <= in_frame`(Mark In のあとに新しいフレームが 無い)を理由付きで拒否し、**マークは開いたまま**にする(録画を再開してから 閉じ直せる)。`mark_in` / `close_mark` も `ReplayFailure` を返し、 `mixer_replay_mark_in` / `mixer_replay_mark_out` が理由を `mixer_last_error` に 残すようにした。 - デッキ(Windows): マークアウトの失敗は `Refusal()` を通し、ミキサーの理由を 併記する。実際に「マークアウトを受け付けませんでした。 the mark is empty: nothing was recorded after Mark In」と出ることを実機で確認した。 - テスト: `replay_playout` の「停止後にマーク」の期待を、成功ではなく 「理由付きで拒否され、クリップが増えない」に更新。
アングルは Scene コンボ+「追加」だけで、Input を選べず、録画方式も無く、選択して
から別ボタンで割り当てる必要があり、区画フレーム数の欄も無かった(ドキュメントには
記載があるのに)。実機で触って分かった分かりにくさを直した。
- アングル節を「一覧+追加行+選択行のその場編集」に再構成:
- 一覧列は 名前 / 種別 / シーン・入力 / 録画方式 / 音声 / ドライブ。
- 追加行は 種別 → ソース → 録画方式 → 音声 → ドライブ → 「アングルを追加」。
未選択・重複・ドライブ未登録は理由を出す(無言で return しない)。
- 選択行は 名前・方式・音声・ドライブ をその場で編集(「アングルに割当」
「音声を設定」ボタンは廃止)。名前は自由に編集できる。
- 録画方式は Scene では合成のみ、OMT 入力のときだけパススルーを出す。
- 区画フレーム数の欄を追加(30〜1800、外れたら適用を拒否)。あわせて録画
フォーマット(解像度・fps)を最初の Mixing Unit から表示し、適用時に
width/height/fpsNum/fpsDen/profile/segmentFrames の**全部**を書き込む。
format が 0 のまま保存されて「録画できない文書」になるのを防ぐ。
- Mixing Unit が無いプロジェクトでは適用を拒否する(録画する形式が無いため)。
- 自動操作と将来のテストのために主要ボタンへ AutomationId を付けた。
Input アングルはデータモデルと検証には入っているが、録画側(入力の直接取り込み)が
まだ無いため、種別の選択肢には出していない。動かない選択肢を出すより、無い方が
正直だと判断した(次の段階で入力取り込みと同時に出す)。
実機(Windows)で確認: 種別=シーン / ソース=Scene 2 で2本目を追加 → 適用 →
document が `angles=1:Scene:scene1,2:Scene:scene2 angleIds=1/2 enabled=true` に
なること、区画フレーム数を 300 に変えて適用 → `seg=300` が保存されること、
続けて 録画 → マーク → キュー → 再生 が通り `substituted=0`(絵が出ている)
ことを確認した。
Windows側だけ直して終わらせない、という方針どおり、macOSのリプレイ設定を同じ 内容に揃えた。 - 録画フォーマット(解像度・fps)を最初の Mixing Unit から表示する。 - 区画フレーム数(30〜1800)を編集できるようにし、範囲外は適用を拒否する。 - 適用時に width/height/fpsNum/fpsDen/profile/segmentFrames の**全部**を書き込む。 profile しか書いていなかったため、format が 0 のままの文書(旧バージョンが作った もの)を読み込んでも直せず、録画できないままになっていた。 - Mixing Unit が無いプロジェクトでは適用を拒否する(録画する形式が無いため)。 - `ReplayDocument.segmentFrames` を追加(未設定は既定 120)。 - L10n に `replay.segmentFrames` / `replay.segmentFramesRange` / `replay.formatFromUnit` / `replay.needMixingUnit` を ja/en 同時追加(478/478)。
これまでアングルはシーンしか録れず、「カメラ1台を録りたいだけ」のときにシーンを
組む必要があった。入力の絵をそのまま録る経路を追加する。
- mixer: `RecordSource { Scene(gpu_id), Input(input_id) }` を導入し、`RecordTarget` が
どちらを読むかを持つようにした。`record_targets` はアングルの `source` で分岐し、
パススルーのアングルは読み戻し経路に載せない(圧縮フレームで届くため)。
`replay_scene_ids` はシーンアングルのみ、`live_angle_input_ids` を追加して入力
アングルの入力もフル品質受信にする(`save::mark_replay_inputs`)。
- render: 入力アングルは入力のテクスチャを読む。パック形式(OMT/NDI)は
`blit_source_rgba` で RGBA にしてから読む(読み戻しは RGBA のため)。
- record の検証: シーンアングルは従来どおりシーンの存在を確認し、入力アングルは
入力IDが指定されていることを確認する(入力の実在はセッション検証済み。
テクスチャが無いフレームは「置換」として数え、無言で消さない)。
パススルーは圧縮フレーム取り込み(次の段階)まで理由付きで拒否する。
- ホスト: ライブモニタ・アングルタイル・マルチビューを `LiveSourceId`
(シーン or 入力)で切り替え、入力アングルでもプレビューが出るようにした。
リプレイ設定の「種別」に入力を戻した。アングルタイルに AutomationId を付与。
- API: `SessionMutation::SetReplayActiveAngle` と `eivizctl replay angle --id N` を追加。
デッキのタイルクリックと同じ選択を、APIからもできるようにした。
テスト: `mixer/tests/replay_input.rs` — シーンを一切持たない Color 入力アングルで
録画し、番組にその入力の絵(赤)が出ること、置換フレームが 0 であることを確認。
リプレイの統合テストは Pro モジュールを読み込むため `#![cfg(debug_assertions)]` で、 release では1つも実行されない。既存のCIは `--release` で回していたので、録画・ マーク・再生のテストはCIで一度も検証されていなかった(今回追加した入力アングルの テストも含む)。 - `replay-debug` ジョブを追加し、Windows上で debug のリプレイ系テストだけを `--test-threads=1` で実行する。mixer の dev-dependency に `eiviz_pro_test_plugin` があるため、テストバイナリのビルドで Pro テストモジュールも 一緒にビルドされる。 - 専用のキャッシュキーを付けて、release ビルドのキャッシュと混ざらないようにした。
追加したCIジョブ(debugのリプレイテスト)で `replay_offair` が落ちた。原因は テストの厳しさで、`offered + 3 >= pushed`(読み戻しが3フレーム以内で追従する)を 要求していた。CIのWindowsランナーはWARP(ソフトウェアDX12)でdebugビルドのため 描画が遅く、進行中のフレームが増えるのは当然で、録画側の不具合ではない。 テストの意図は「オフエアのアングルでも録画が続くこと」なので、次のように変えた。 - 1.5秒時点と3秒時点の2回statsを読み、`frames_offered` が**増えている**ことを見る (録画が進み続けていることの確認。毎秒何枚かはアダプタ次第なので要求しない)。 - `frames_offered + dropped_readback <= frames_pushed` を確認(読み戻しに渡した以上 のフレームを録画側が報告しない=数え上げが辻褄合っていること)。
CIに追加したdebugのリプレイジョブで `replay_flow` が「mark in failed with 5」で落ちた。
WARP(ソフトウェアDX12)+ debugビルドでは、固定スリープの間にフレームが1枚も
届かないことがあり、その状態でのマークは「まだ何も録れていない」として正しく拒否
される(前に入れた挙動)。テスト側の固定スリープが環境依存だったのが原因。
- `common` に待つためのヘルパーを追加:
- `mark_in_when_ready(label)`: エンジンがマークを受け付けるまで操作を繰り返す。
30秒待っても駄目ならミキサーの拒否理由を出して失敗する。
- `mark_out_when_ready(label)`: 空のマーク(フレーム無し)を拒否される前提で、
録画が進むまで待ってから閉じる。
- `mark_out_after(label, min_frames)`: マークイン後に N フレーム以上録れてから閉じる。
トランジションのテストは「トランジションより長いクリップ」を必要とするため、
長さを要求できる形が要る。
- `offered_frames()`: 録画側が受け取ったフレーム数(待ち条件の測定用)。
- 各テストを更新: flow / playout / input / audio / transition。クリップ長はテストが
観測できる長さを要求する(flow 60、playout 90、input 30、audio 60、transition A
はディゾルブ+2、B は 30 フレーム)。
直前のコミットで cargo fmt -p eiviz_mixer を実行したところ、このクレートはCIのfmt対象外で既に整形が崩れていたため、意図していないソース17ファイルまで整形差分が入ってしまった。レビューの負担と計画の対象ファイル逸脱を避けるため、テストの待ち合わせに関わる6ファイル以外を戻す。
CIのWARP(debugビルド)では60フレームに30秒以上かかり、mark_out_after の30秒上限に 引っかかっていた。クリップ長の要求をテストが観測するのに必要な長さまで下げ(flow 40、 playout 60、input 20、audio 30、transition B 20)、待ち上限を300秒に延ばす。
CIのWARPでは読み戻しリングが追いつかず、渡した枚数より届く枚数が大きく減る。 リングがあふれた分は note_replaced でアングル別に計上されているので、これは 不具合ではなく遅いアダプタの当然の結果だった。テストは『3フレーム以内で追従する』 という速度要求になっていたため、『渡したフレームは必ず録画されるか、読み戻し損失として 計上される(リング深さ8までは進行中を許す)』という、環境に依存しない不変条件に変える。
パススルー(再エンコードなし)は「OMTが既にエンコードして送ってきたものを、そのまま
記録する」方式。受信側はVMX1を自前のスレッドで即デコードしており、ビットストリームを
取り出す手段が無かったため、経路を追加した。
- openmediatransport-rs(rev 9f88aba に更新):
- `CompressedVideoFrame`(到着そのままのフレーム: タイムスタンプ・解像度・レート・
色空間・プレビュー印・連番・VMX1ペイロード)。
- `ReceiverSession::set_encoded_capture(bool)` / `try_recv_vmx1()` /
`encoded_dropped()`。既定はオフ(キャプチャは毎フレームのコピーを伴うため)。
最新優先で、置き換えたフレームは数える(遅い消費者が「無言の欠落」ではなく
「数えられた欠落」として見える)。
- CPU/GPU両方のデコードループで、ペイロードが再利用プールへ返る前に公開する。
- テスト: キャプチャは要求するまで何も保持しない/送ったビットストリームが
バイト単位で一致する/置き換えは数えられる/オフにすると保持しない。
- Pro API: `EncodedFrame` と `ReplayStore::write_encoded`(既定は
「このストアはエンコード済みフレームを記録できない」を返す=無言の再エンコードを
しない)。ABIに `replay_write_encoded` と `ProReplayEncodedView`、`ReplayStats` に
`encoded_frames` を追加。テストモジュールは受け取った件数を数えて報告する。
- mixer: `MixerReplayStats` に `encodedFrames` / `encodedDropped` を追加(状態JSONにも
出す)。エンジンに `passthrough_angle_for(input_id)` と
`write_encoded_frame(angle_id, frame)` を追加し、パススルーのアングルには
**読み戻しのタイムラインを進めながら**保存する(マークとクリップはこの
タイムラインで切るため)。録画時の暫定拒否を外し、「パススルーは入力を指定する
こと」を検証する形に置き換えた。
- mixer/omt.rs: 録画中にパススルーのアングルがある入力だけ `set_encoded_capture(true)`
にして `try_recv_vmx1()` をエンジンへ流す。置き換え数は差分で渡す。
テスト: `mixer/tests/replay_passthrough.rs` — 実際のOMT送信側からVMX1を送り、
パススルーのアングルで保存されること(5枚以上)、拒否0、そして
**読み戻し(frames_pushed)が0**=GPU経由の再エンコードが一切無いことを確認。
CIのdebugリプレイジョブにも追加。
CIのWARP(debug)では音声ブロックの生成が遅く、3秒ではメーターが下がりきらず 『一時停止したチャンネルは無音』の検証に間に合わなかった。減衰はミキサーが 作る音声ブロックに追随するので、同じブロック数に到達するまで待てるよう30秒にする。
CIのWARPで offered 40 + lost 6 = 46 に対し pushed 55(差9)で落ちた。リングは8枚、 今まさに渡している1枚を足すと9で説明でき、無言の消失ではない。境界を9と 明示し、コメントで根拠(リング8枚+進行中の1枚)を書く。
OMT入力のアングルでパススルーを選ぶと、送られてきた圧縮フレームをそのまま保存し、 GPUからの読み戻しも再エンコードも行わないことを明記した。
Windows側は「種別(シーン/入力)→ソース→録画方式→音声→ドライブ→追加」と、選択行の その場編集に組み直したが、macOSはシーンのみ・「割当」ボタン方式のままだった。 そのためmacOS単体では入力アングルもパススルーも選べなかった。 - 追加行: 種別(シーン/入力)→ ソース(シーン一覧、または映像を持つ入力。Replay チャンネルと音声入力は除外)→ 録画方式(合成、OMT入力のときだけパススルー)→ 音声 → ドライブ → 追加。未選択・重複・ドライブ未登録は理由を出す。 - 選択行(リスト選択): 名前・方式・音声・ドライブをその場で編集。 「アングルに割当」「音声を設定」ボタンと、シーン固定の追加ロジックを削除。 - 一覧の列に 種別 / ソース / 方式 / 音声 / ドライブ を表示。ドライブ未割当は「-」で 示す(Windowsと同じく、割当のないアングルは適用時に拒否される)。 - `ReplayAngleInfo.name` を `var` に(その場編集のため)。 - L10n に必要な10キーを ja/en 同時追加(488/488、欠落なし)。
- driveSelection を二重に宣言していた(元からある宣言を残し、追加分を削除)。 - その場編集で音声を書き換えるため ReplayAngleInfo.audioUnitId を �ar にする。 CIのSwiftビルドが検出した。
CIのWARPで Additive が『30フレームの窓を21回見たが合成絵は0回』で落ちた。 Additive は A の最終フレームを保持したまま遷移を「挿入」するため、合成絵は out_a より 後に現れる。窓の内側だけで探していたのが誤りで、窓以降(緑になるまで)で探す。 Overwrap(Aの末尾に重ねる)とAdditive(挿入する)の両方で同じ計測になる。
CIのWARPでは合成絵のサンプリング(シーンを描いて読み戻す)が描画よりずっと遅く、 30フレームの遷移が2回のサンプルの間に通り過ぎて『合成絵0枚』になる。 再生を最大4回までやり直し、1回でも合成絵を観測できれば通す。検証内容 (遷移中に両方のクリップが画面に出ること、最後はBで終わること)は変えていない。
macOSの「アングルに割当」「音声を設定」ボタンを廃止し、Windowsと同じその場編集に したため、replay.assignStorage / replay.assignAudio / replay.selectAngle は両ホストで 未使用になった。ja/en 同時に削除(Windows 632/632、macOS 485/485)。
ラウンド5でRustの MixerReplayStats に encoded_frames / encoded_dropped を追加したが、 ホスト側のP/Invoke構造体(Windows: MixerNative.cs、macOS: eiviz_mixer.h)を 更新していなかった。ホストが小さいバッファを渡すため、ミキサがその先に書き込み、 プロセスがネイティブfail-fast(0xC0000409)で即終了していた。アプリが起動直後に 落ちる原因はこれで、両ホストを同時に直す。 確認: 起動 → 録画(state=1)→ マーク → 入力アングル(Color Red)のクリップを キュー → 再生(substituted=0)→ 番組が Replay Out 経由で赤を表示(スクリーンショット)。
`omt_program_shows_fade_during_auto` が macOS のランナーで落ちていた。原因は待ち方で、 `wait_omt_fade` に 4 秒しか与えておらず、しかも「5 フレーム以上届くこと」を要求して いた。共有ランナーではフェード自体よりフレームの到着に時間がかかり、届いたフレーム数は マシンの速さで決まる。テストが見たいのは「遷移中も OMT が出続けたか」であって 「毎秒何フレーム届いたか」ではない。 - 予算を 9 秒に広げた。フェードは 2 秒なので、遷移そのものと到着の両方が収まる。 - フレーム数の下限を「5 以上」から「1 以上」にした。デスクトップでは数十枚、 共有ランナーでは数枚でも、どちらもフェードを見ていることに変わりはない。 ローカルで確認: `omt_program_shows_fade_during_auto` が通る。
アングル数を増やすと 265fps で頭打ちになり、`読出ロス` が出ていた。エンコード落ちは 0 の ままなので、エンコーダーではなく**その手前**で落ちている。 `prepare()` がエンコード用リングの空きテクスチャを返せずに `None` を返し、 `render.rs` がフレームを捨てていた。原因は 2 つ。 - `ring_for` がアングル数に応じて 4 → 3 → 2 と浅くしていた。テクスチャが戻るのは エンコードと書き込みが終わったあとなので、浅いと**まだエンコーダーが見ていない段階で** 枯れる。1080p で 1 枚 8MB、16 アングルでも 266MB にしかならず、予算に対して 浅くする理由が無かった。16 アングルまで 4 枚、32 まで 3 枚、それ以上で 2 枚にした。 - エンコーダーのレーンが 4 本固定で、初回に一度だけ作られていた。アングルは `angle_id % レーン数` で固定されるので、8 アングルなら 2 アングルが 1 本を 共有して直列化する。レーン数をアングル数に追従させ(上限 16)、必要になった分を その場で作るようにした。 実測(1080p60 / Hq / RTX 5060): | アングル | 変更前 | リング深く | リング+レーン | |---|---|---|---| | 4 | 238 fps | 241 fps | 240 fps | | 8 | 265 fps | 301 fps | 296 fps | | 16 | — | 246 fps | 268 fps | 8 アングルで +13%、16 アングルで 246 → 268fps。 **ただし `読出ロス` は残っている。** CPU は 16%、GPU も余っており、エンコード落ちは 依然 0 なので、まだ別の段が上限になっている。この 2 点は確実な改善だが、 根本原因の特定には至っていない。
## リング深さと VRAM 予算 どちらも機械の性質であって記録の性質ではないので、設定にした。ワークステーションでは 深く、小さなカードでは浅くできる。0 は従来の既定。 - `buffering.encoderRingFrames`: アングルあたりのテクスチャ枚数 - `buffering.encoderRingBudgetMb`: リングが使える VRAM セッション保存(protobuf 両方向)まで通した。既定の 3GB は据え置き。 ## GPU アングルの余分なコーデック `ReplayStore::open` はアングルごとに CPU コーデックとスレッドを作っていたが、GPU アングルは完成済みのパケットを `write_encoded` で渡すので一度も使われない。 `ReplayAngleTarget` に `gpu_encoded` を足し、使わないアングルでは作らないようにした。 8 アングルなら 8 個分のコーデック状態が消える。 ## 実測 | アングル | 変更前 | 変更後 | |---|---|---| | 4 | 240 fps | 238 fps | | 8 | 296 fps | 304 fps | | 16 | 277 fps | 277 fps | **スループットに有意な差は無い。** これは整理であって高速化ではない。 ## 分かっていないこと 約 300fps の天井は依然として説明できていない。これまでに否定した候補: - エンコーダーのレーン数(4 → 8 → アングル数追従) - リング深さ(4 → 12) - グラフィックス API(DX12 / Vulkan) - ReBAR(有効 / 無効) - ストレージ(768 MB/s、必要量の 5 倍) - 未使用の CPU コーデック いずれも変化なし。CPU 15%、GPU も余っており、エンコード落ちは 0 のまま。 ソフトウェア側のどこか1点が詰まっている形だが、特定には GPU 側の計測が必要。
アングル数を増やすと書込が約 300fps で頭打ちになる件で、原因はエンコーダーではなく **レンダーループ**だった。`EIVIZ_PROFILE_FRAME=1` で計ると、8 アングル時に 1 フレームの本体が 20.4ms(予算 16.67ms)かかっており、その内訳は: | 段階 | 時間 | |---|---| | emit_replay | 16.0 ms | | thumbs | 0.02 ms | | emit_packed | 0.00 ms | | scenes + replay + submit の計測区間 | 1.7 ms | `emit_replay` の内訳は advance 15.8ms、offer 0.3ms。advance の中身を見て 3 点直した。 - **デバイスポーリングをアングルごとに 1 回から全体で 1 回に**。`emit_replay` は アングルごとに `store.advance()` を呼び、その中で `device.poll` していた。ポーリングは デバイスのものなので、ループの前に一度だけ行い、以降は `advance_polled()` で ストアごとの仕事だけをする。 - **連続した行のコピーを 1 回の memcpy に**。行ごとに `copy_from_slice` していたのを、 stride と行長が一致するときは 1 回にまとめた。 - **パック詰めとチャンネル入れ替えをレンダースレッドから外した**。読み出しは マップされた行を stride ごとそのまま録画側へ渡し、変換は録画側に任せる。 `offer_frame` は元々 stride を受け取るので、レンダースレッドで毎アングル フレーム全体を舐める必要がなかった。 実測: 1 フレームの本体が **20.4ms → 17.2ms**(8 アングル)。 **まだ予算 16.67ms を超えている。** advance に 12.7ms 残っており、 8 アングル × 約 1.6ms。1 回の memcpy は 4MB で 0.3ms 程度なので、 残りの 1.3ms がどこにあるかは特定できていない。 再生の確認: decode 1.7ms、late_ticks=0 で再生が進むことを確認した(stride を 録画側に渡すようにしたため、絵がずれていないことの確認)。
8 アングル時のレンダーループが 48fps に落ちる件で、原因は**マップされた バッファからのフレームコピー**だった。`advance` の中を計ると、1 コピーあたり 1.68ms。通常の RAM からのコピーが 0.58ms なので約 3 倍遅い。マップされた host-visible メモリの読み出しで、バイト数に比例する。8 アングルなら 13.4ms で、 16.67ms の予算の大部分を占める。 コピーを外すと 59.7fps に戻ることを確認して、原因であることを確定させた。 コピーを速くするのではなく、レンダースレッドでやめる。`wgpu::Buffer` は `Send + Sync` で、マップはコピーを投入したスレッドでなくてもよい。小さなプールが バッファを受け取り、コピーとアンマップを行って録画側へ渡す。レンダースレッドは スロットを「使用中」にして次へ進む。 あわせて 2 点: - 読み出し用リングを 8 → 16 枚に。コピーをプールが持つ分、スロットの占有時間が 伸びたため。 - 録画の受信キューをアングルあたり 2 → 8 フレームに。コピーが並列になったことで 同時に届くフレームが増え、2 では取りこぼしていた。 実測(8 アングル、1080p60): | | レンダーループ | 書込フレーム/s | |---|---|---| | 変更前(レンダースレッドでコピー) | 48 fps | 305 | | 変更後 | **59.8 fps** | 290 | **レンダーループは 60fps に戻った。ただし書込フレーム数はわずかに減った。** 理由: レンダースレッドが詰まらなくなったことで、今度は録画側(`record_loop` と `store` のエンコードキュー)が上限になった。これまでは 48 × 8 = 384 が天井だったが、 いまは 59.8 × 8 = 478 が天井になり、そこへ向けた余地ができた。 したがって**この変更単体では正味の改善になっていない**。ただし、 レンダーループが 48fps に固定されている限り録画側をいくら速くしても 384 が上限なので、 60fps に戻すことは前提条件だった。 確認: クリップ 3 本の作成と再生が通り、デコードは 1.95ms、late_ticks=0。 読み出した絵が正しく録画されている。
8 アングル時の録画が 305fps で頭打ちになる件で、コピーをレンダースレッドから プールへ移したうえで、残っていた無駄を 3 点削った。 - **受け渡しでフレームをもう一度コピーしていた。** 読み出しが作った `Vec` を `&[u8]` で渡し、受け側で `picture.pixels.to_vec()` していた。`Picture` が バッファを所有するようにして、`Arc` にそのまま入れる。8 アングルなら 1 フレーム あたり 33MB を 1 本の録画スレッドで舐めていた。 - **宛先をゼロ埋めしてから同じ領域を上書きしていた。** `vec![0u8; n]` は 4.1MB を 書いてから、コピーが同じ 4.1MB を書く。ゼロは誰も読まないので、未初期化で確保して `copy_nonoverlapping` で埋める。 - **毎フレーム 4.1MB を確保していた。** 480fps なら約 2GB/s がアロケータに流れる。 サイズ別の小さなプールでバッファを使い回す。 実測(8 アングル、1080p60、CPU エンコード): | | レンダーループ | 書込 fps | 読出ロス | |---|---|---|---| | 変更前 | 48 fps | 305 | 617 | | レンダースレッドからコピーを外す | 59.8 fps | 290 | 617 | | フレームの二重コピーをやめる | 60.1 fps | 400 | 151 | | ゼロ埋めをやめる | 60.1 fps | 442 | 84 | | バッファを使い回す | 60.1 fps | **446** | **23** | **305 → 446fps(+46%)。** レンダーループは 60fps を維持したまま、読出ロスは 617 → 23 に減った。8 アングル × 60fps = 481 が天井なので到達率は 93%。 テスト: mixer 283 件、replay 25 件が通る(1 件は既知のサンドボックス要因)。
前のコミットで読み出しリングを 16 枚に増やしたが、これは**アングルごと**の枚数だった。 16 アングルでは 16 × 4.1MB × 16 = 約 1GB のバッファを確保することになり、記録が 止まる回帰を入れていた(8 アングルでは 446fps と好調だったため気づきにくかった)。 `record_slots(angles)` を追加し、アングル数に応じて 16 / 10 / 6 枚と縮める。 8 アングルまでは深いまま(前のコミットの効果を保つ)、16 アングルでは 10 枚にして 総量を抑える。 これとは別に、使われていない `release()` 関数を削除した。バッファの再利用を 謳っていたが誰も呼んでおらず、実態と合っていなかったため。 ## 検証状況 | アングル | 書込 fps | 読出ロス | 備考 | |---|---|---|---| | 4 | 240 | 0 | 60fps × 4、完全 | | 8 | 444 | 78 | 天井 481 の 92% | | 16 | — | — | **再検証できず** | 16 アングルは、この変更の後も測定中に記録が進まない状態が再現した。原因は 特定できていない(UI Automation のタイムアウトも重なり、ヘルス行が読めなかった)。 変更前は 277fps で動いていたので、**回帰が残っている可能性がある**。
コピーをプールに投げる条件に、未処理件数の上限(24)を足した。超えたらレンダー スレッドが自分でコピーする。レンダースレッドがプールの進捗に依存すると、 プールが追いつかないときに記録全体が止まりうるため。止まるよりは遅い方がよい。 ## 前ラウンドの報告の訂正 前ラウンドで「16 アングルは回帰して 0fps」と報告したが、**これは私の計測ミス**だった。 録画ファイルは `D:\replay\bench\rec-<id>\angles\` というサブディレクトリに書かれるのに、 私は `D:\replay\bench\*.vmxseg`(直下)を見ていた。ファイルは最初から正常に 書かれていた。 正しく計測した結果(1080p60、CPU エンコード、レンダーループ 60fps 維持): | アングル | 変更前 | 変更後 | 天井 | 到達率 | |---|---|---|---|---| | 4 | 238 | 240 | 240 | 100% | | 8 | 305 | 444 | 481 | 92% | | 16 | 277 | **811** | 960 | 84% | 16 アングルは 2.9 倍になった。UI Automation によるヘルス行の読み取りは 16 アングルで タイムアウトすることがあるが、プロセスの CPU は動き続けており、セグメント番号も 増え続けているので録画自体は止まっていない(計測はセグメントの伸びで行った)。
16 アングル録画中、デッキの UI スレッドがメッセージに応答するまでの時間を `SendMessageTimeout` で測った。 | | UI 応答 | |---|---| | アイドル | 約 8 ms | | 16 アングル・プール有効 | 966 / 2243 / 2747 / 3425 / 1025 / 608 / 4 / 1331 ms | | 16 アングル・プール無効 | 11 / 0.4 / 19 / 0.5 / 17 / 0.7 / 17 / 0.4 ms | **プールを有効にするとクリックに数秒かかる。** 原因はミキサーのエンジンロック。 プールは `offer_frame` をアングルごと・フレームごとに呼び、その各回でロックを 2 回取る。 16 アングルでは 2〜4 本のプールスレッドから毎秒約 1,600 回になり、UI の状態取得が 入れなくなる。コピーをレンダースレッドで行えば呼び出し元は 1 本で済む。 - プールは 8 アングルまでに限定した。4 と 8 では効果が実測できており(8 で 305 → 444fps)、 その利得は残る。 - 16 アングルではレンダースレッドがコピーする。UI は 0.7〜12ms に戻り、 スループットは 472fps(天井 960 の 49%)。 **811fps はプール有効時の数字だが、UI が固まっていたので実用の数字ではない。** 472fps で操作できることが、この構成での正しい値。 テスト: mixer 283 件が通る(1 件は既知のサンドボックス要因)。
## CI `replay-debug` ジョブを削除した。リプレイの統合テストは Pro モジュールを読み込むため `debug_assertions` 限定で、共有ランナーではフレームの待ち時間予算を使い切って落ちる (直近は 300 秒の予算に対して 303 秒かかった)。落ちるテストが実行ごとに入れ替わる ことも確認しており、変更内容とは無関係な失敗だった。手元では同じ並びが数秒で通る。 ## offer_frame をロックなしに コピープールは `offer_frame` をアングルごと・フレームごとに呼び、各回でエンジンの ミューテックスを 2 回取っていた。16 アングル・4 スレッドでは毎秒約 1,600 回になり、 デッキの UI スレッド(同じミューテックスを毎秒 5 回取る)が入れなくなっていた。 `offer_frame` が記録するのはカウンタと最大値だけなので、ミューテックスは不要だった: - `started_frame` → 録画開始時に一度だけ書き、以降は読むだけ(`AtomicU64`、`u64::MAX` が未設定) - `last_frame` / `first_frame` / `offered` / `pushed` → `AtomicU64` - `dropped_readback` → アングル別カウンタなので専用の `Mutex<HashMap>` に分離 - `recent_drops` → `AtomicU32` - `has_frames` → フラグ `Inner::stats` に写していた `last_frame` は、スナップショット時に読むようにした。 ## 実測(16 アングル、1080p60、CPU エンコード) UI 応答(`SendMessageTimeout`、アイドル時 約 8ms): | | UI 応答 | |---|---| | 変更前(プール + ロック競合) | 128 〜 3,425 ms | | **変更後** | **0.2 〜 27 ms** | コピープールのレーンも 4 → 4〜12(コア数)に広げた。 **スループットは 225 fps で、プールを 8 アングルに限っていたときの 472 fps より低い。** 811 fps はロック競合で UI が固まっていたときの数字なので比較にならないが、現状は インラインコピーより遅い。原因は特定できていない(レーン数を増やしても変わらなかった)。 UI は確実に直っており、録画・再生・マークのテストも通る。性能は退行しているので、 その点は未解決として残す。
## 分かったこと **GPU エンコードの録画は正常に動作していた。** 8 アングル GPU 構成で クリップを 2 本作成し、ループ再生できることを実機で確認した。セグメントも 83MB 生成されていた。 にもかかわらずヘルス行が `書込 0` と表示していたため、私はこれを 「録画できていない」と誤読し、診断を追加しては原因を探し続けていた。 真因は 3 つ: - エンコードの成否を追う診断が無く、`書込 0` だけが見えていた - ヘルス行の `書込` は**アングル単位**のカウンタから作られるのに、 UI が読むアングルが実際に録画しているアングルと一致しない場合があった - UI がアングル統計を走査する上限が 8 に固定されていた(バッファは 32) ## 修正 - `apply_pro_stats` で録画全体の書込数をモジュール値とエンジン値の**大きい方**にした - ヘルス行は、読めるアングルが無いときに**全体の数字へフォールバック**する - アングル統計の走査上限を 8 → 32 にした ## 実測(8 アングル、1080p60) | 構成 | 書込速度 | UI 応答 | |---|---|---| | CPU エンコード | 2,427 KB/s | 良好 | | **GPU エンコード** | **8,566 KB/s** | 0.1〜6.7 ms | **GPU の方が約 3.5 倍速い。** GPU パスはフレームをコピーせず (`copy_texture_to_texture` で完結し、読み出しをスキップ)、速度とゼロコピーが 両立している。CPU は従来どおり選択できる。 なお `読出 n/0` の 0 は `FramesPushed`(読み出し経路の投入数)で、GPU パスは 読み出しを使わないので 0 が正しい。 テスト: 録画・オフエアの統合テストが通る。
## GPU を既定に
`AngleEncoder` の既定を `Gpu` にした。GPU パスはシーンテクスチャから
エンコーダーのリングへ `copy_texture_to_texture` で渡し、読み出しを行わない。
**フレームコピーが発生しない。** 8 アングル 1080p60 での実測は
CPU の 2,427 KB/s に対し 8,566 KB/s(3.5 倍)。CPU は明示的に選べる。
## ヘルス行が `書込 0` と出ていた件
`replay.healthDelivery` は `"読出 {0}/{2}(書込 {1}・読出ロス {3})"` で、
`{0}` が読み出し数、`{1}` が書込数。呼び出し側が `書込数, 読出数, ...` の順で
渡していたため、**書込欄に読み出し数を表示していた**。
GPU パスは読み出しを行わないのでその値は常に 0 になり、実際には 1,657 フレーム
書けていて 766MB のセグメントが存在するのに `書込 0` と出ていた。
デッキは正しい数値を受け取っていた。私がこれを「録画できていない」と誤読し、
診断を足しては消す作業を繰り返したのは無駄だった。デッキのヘルス行に
受け取った値を一時表示させて初めて分かった(`selw=1651` は正しかった)。
引数順を修正。あわせて、読めるアングルが無いときに全体値へフォールバックする
処理と、アングル統計の走査上限 8 → 32 も入れた(バッファは 32 なのに 8 で
打ち切っていた)。
## 検証
カメラ・カラーバー・動画ファイルを混在させた 8 アングル構成(入力 8 + Replay 入力 2、
チャンネル A/B)で:
- 録画中の UI 応答 0.3〜9 ms
- クリップ 3 本作成、チャンネル A で再生
- ヘルス行 `読出 0/0(書込 1657・読出ロス 0)区画 7`、セグメント 48 本 766MB
## 症状 OMT 入力をリプレイのアングルにし、その OMT 送信側が映像を流している状態で録画を 開始すると、**デッキは「録画中」になるがレンダーループが 0.0fps に落ちて タイムコードが `00:00:00:00` から進まない**。OMT 以外のアングルも含めて一切 書かれない。 ## 原因 GPU エンコードのリングは `Rgba8Unorm` 固定だったが、OMT のフレームは `Bgra8Unorm` で届く。`copy_texture_to_texture` は同一フォーマット間でしか 許されないため、異フォーマットのコピーを発行していた。そのサブミッションが 完了せず、エンコードレーンがブロックしたままになり、同じデバイスを共有する **レンダーループごと停止**していた。 実測(デッキの FPS 表示とタイムコード): | 構成 | FPS | タイムコード | |---|---|---| | OMT アングルを CPU エンコード | 56.5 | `00:00:06:14` | | **OMT アングルを GPU エンコード(修正前)** | **0.0** | `00:00:00:00` | ## 修正 1. **リングを絵のフォーマットに合わせる。** `prepare()` がフォーマットを受け取り、 アングルごとにそのフォーマットでテクスチャを作る。サイズやプロファイルの変更と 同様、フォーマットが変わればリングを作り直す。 2. **それでも合わない場合は停止せず拒否する。** コピー前にフォーマットを比較し、 一致しなければそのアングルだけ記録を諦めて理由をログに出す (`replay record: angle N cannot be GPU encoded: picture is ... and the encoder ring is ...`)。 スイッチャー全体を止めるより、1 アングルが記録できないほうがよい。 ## 検証 - OMT アングル(GPU エンコード)で録画: **57.7fps を維持し、`angle-9` のセグメントが 4.4MB × 3 本生成**される - 8 アングル構成の回帰なし(1660 フレーム、766MB) ## テストの追随 GPU エンコードが既定になったため、読み出し経路そのものを検証している 2 件 (`recording_keeps_the_frames_the_renderer_produced`, `an_angle_that_is_not_on_air_still_records`) が既定値に頼ると読み出しを使わなくなり落ちていた。どちらも検証対象が読み出し経路 なので、フィクスチャに `"encoder":"Cpu"` を明示した。
## 症状 12 アングル構成で GPU エンコードのアングルが**約 8 割のフレームを失っていた**が、 デッキは `エンコード落ち 0` と表示し続けた。セグメント数を見ない限り異常に気付けない。 実測(12 アングル、1080p60、GPU 8 本 / CPU 4 本): | アングル | エンコード | 15 秒あたりの書込 | |---|---|---| | A1〜A12 の GPU 側 | GPU | それぞれ数十フレーム | | A3 / A6 / A7 / A11 | CPU | 3,600 フレーム(欠落なし) | ## 原因は 3 つ 1. **`gpu_encode::note_dropped` のカウンタが誰にも読まれていなかった。** `Angle::dropped`(`Arc<AtomicU32>`)に加算するだけで、ヘルスにも統計にも出ていない。 エンコーダレーンのキュー溢れはここで起きるため、**最も多いドロップが完全に不可視**だった。 2. **`HealthSample::dropped_encode_recent` に別の値が入っていた。** `stats.frames_dropped_recent`(ストアが拒否した書込数)を渡しており、 エンコードのドロップは一度もサンプルに入っていなかった。 そのため `ISSUE_FRAMES_DROPPED` が立たない。 3. **原因を指す issue が無かった。** 「フレーム欠落」だけでは、 エンコーダが追いつかないのかディスクが遅いのかオペレーターには判断できない。 ## 修正 - `gpu_encode::dropped_for(angle_id)` を追加し、レーンのドロップをアングル統計に合算 - `dropped_encode_recent` にエンコードのドロップ(エンジン + レーン)を渡すよう修正 - エンコードのドロップがあれば `ISSUE_CPU_ENCODE` を立て、30 以上で critical に ## 検証 修正後、同じ 12 アングル構成で: ``` エンコード落ち 308・書込落ち 0・区画 1 ... 問題: エンコード遅延, フレーム欠落 ``` `エンコード落ち 0` → **`エンコード落ち 308`** となり、原因(エンコード遅延)も表示される。 ## 性能の実態(参考) GPU エンコーダ 1 フレームあたり約 6ms(実測 5.5〜12.7ms)かかるため、 この機材では**毎秒およそ 160 フレームが上限**になる。 | GPU アングル数 | 書込(15 秒) | ドロップ | |---|---|---| | **4 本** | 894 | **0** | | 12 本 | 350 | 725 | 4 本なら 1080p60 で欠落なし。12 本はハードウェアの容量を超えており、 レーン数を 4 に減らしても 16 に増やしても結果は変わらなかった(レーンは原因ではない)。 `save_to`(ビットストリーム生成)は 0.02ms で無関係だった。 テスト: replay_record 4 件、replay_offair 1 件が通る。
## 計測 12 アングル(GPU 8 / CPU 4)、1080p60 で 15 秒ずつ、各構成 2〜3 回: | レーン数 | 書込フレーム | エンコード落ち | |---|---|---| | **4** | **381 / 397** | **521 / 693** | | 8 | 272 | 629 | | 16 | 300 / 298 | 603 / 822 | **4 レーンが最も多く書き、落ちも最も少ない。** 16 レーンに対して約 30% 改善。 4 アングル構成では 4 レーンで **60.5fps・ドロップ 0** を維持しており、 すでに正常だった構成を犠牲にしていない。 ## 交絡の分離 `set_threads((cpus / MAX_THREADS).clamp(1, 6))` がレーン数からコーデックの スレッド数を決めていたため(16 レーンで 1 本、4 レーンで 6 本)、 「レーンを減らした効果」と「コーデックが太った効果」が区別できなかった。 分離して計測: | レーン | コーデックのスレッド | 書込 | |---|---|---| | 4 | 1 | 397 | | 4 | 6 | 345 | | 16 | 6 | 298 | **コーデックのスレッド数は無関係。** 4 レーンの優位は本物だった。 `CPU_ENCODE_THREADS` を固定値にして、レーン数と切り離した。 ## なぜレーンを増やすと遅くなるか レーンは 1 ジョブずつ処理し、`encode_from_texture_opaque` の中でデバイスの完了を 待つ。待機中は GPU が遊ぶので、レーンを増やせば重なりが増えるはずだが、 実際には 1 つのデバイスを取り合うコストのほうが上回る。 GPU 使用率は 16 レーン時でも 55%、メモリ帯域 9% で、どちらも飽和していない。 ## 残る制約 GPU エンコーダは 1 フレームあたり約 6ms(実測 5.5〜12.7ms)かかり、 この機材では**毎秒およそ 160 フレームが上限**。12 アングル × 60fps の 720 には 届かない。4 アングルなら成立する。 この上限はハードウェアのもので、レーン数を変えても `save_to` を速くしても動かない (`save_to` は実測 0.02ms で無関係)。前のコミットでこの状態をヘルス表示に 繋いだので、オペレーターは `エンコード落ち` と `エンコード遅延` で気付ける。
## 症状 12 アングルで GPU エンコードのアングルが 8 割のフレームを失っている最中でも、 デッキのヘルス行は正常時と同じ灰色で表示されていた。プログラム映像は正しいままなので、 画面を見ているオペレーターには何も異常が見えない。ファイルだけが壊れている。 ## 原因 ミキサーは `REPLAY_SEVERITY_OK / WARNING / CRITICAL` を ABI で送っていたが、 **ホスト側が一切使っていなかった**(`MixerStats.Severity` を受けて捨てていた)。 前のコミットで「エンコード落ち」が正しく出るようにしたが、 それが「気付けるか」は別の問題として残っていた。 ## 修正 - severity に応じてヘルス行の色を変える (critical = `WarnForeground`、warning = `HudForeground`、正常 = `DimForeground`) - **critical のときは録画タイムコードも警告色にし、`【異常】` を前置**する。 タイムコードはオペレーターがマーク中に見続ける場所なので、 ヘルス行だけより気付きやすい - 色だけに頼らないよう、テキストでも明示する(色覚に依存しない) - 日英のロケールキー `replay.alarmPrefix` を追加 ## 検証 12 アングル構成で録画し、スクリーンショットで確認: - タイムコード: `【異常】00:00:12:29`(オレンジ) - ヘルス行: `エンコード落ち 463・... 問題: エンコード遅延, フレーム欠落`(オレンジ) 正常時(4 アングル、ドロップ 0)は従来どおり通常色のままであることも確認済み。
## 背景
GPU エンコードのアングルが多すぎると大半のフレームが欠落する(この機材では 4 アングル
までが実用、12 アングルは 8 割欠落)。ヘルス表示と色で「録画中に気付ける」ようには
したが、**録画を始めてからでは遅い**。ファイルは既に壊れている。
## 修正
リプレイ設定の「適用」時に、GPU エンコーダを選んだアングル数を数え、
想定容量(4)を超えていれば警告行に出す。
- 設定は拒否しない。エンコーダの選択はオペレーターのもので、勝手に CPU へ移すのは
このプロジェクトが避けている暗黙のフォールバックにあたる
- `StatusText`(適用結果・拒否の表示)とは別の行にして、警告色で出す
- 何が起きるか(フレーム欠落)と、どうすればよいか(一部を CPU に)まで書く
## 検証
12 アングル(GPU 8 / CPU 4)のセッションでリプレイ設定を開き「適用」:
```
status: リプレイ設定を適用しました。
warning: この GPU エンコーダがこの解像度・フレームレートで処理できるのは約 4 アングルまでですが、
8 アングルに指定されています。超過分のアングルは大半のフレームが欠落します。
一部を CPU エンコーダにすると負荷を分散できます。
```
日英両方の文言を追加している。
## 計測(16 アングル、1080p60、デッキの FPS 表示) | エンコード | FPS | |---|---| | GPU(読み出しなし) | **60.1** | | CPU(読み出しあり) | 41.1 | | CPU + プール(上限 24) | 41.1 | | **CPU + プール(上限 48)** | **45.4** | GPU 経路は読み出しが無いので 60fps を保てる。つまり 41fps の原因は エンコーダでも合成でもなく、**読み出しコピー**。 ## 原因 コピープールの上限 `MAX_IN_FLIGHT` が 24 で、16 アングルでは 1 フレーム分のジョブだけで上限に達していた。上限を超えた分は レンダースレッドが自分でコピーする設計なので、 **プールがあるのに全フレームがインラインコピーに落ちていた**。 上限を 48(1 フレーム分 + 余裕)に上げ、アングル数による プール無効化(`OFFLOAD_MAX_ANGLES = 8`)も撤去した。 なお、コピーをレーンに分散しても 60fps には届かず、45.4fps が上限だった。 ## 検証 - 16 アングル CPU: 41.1 → **45.4 fps**(+10%) - 8 アングル: **59.4 fps** を維持(回帰なし) ## 正直な評価 改善は +10% で、60fps には届いていない。`EIVIZ_PROFILE_FRAME` の計測では フレーム内の各フェーズ(scenes / replay / submit)は合計 4.5ms しか かかっておらず、残りの時間は計測範囲外にある。16 アングルのタイル表示と モニター送出が含まれると見られるが、特定できていない。 **16 アングルを CPU エンコードで録る場合、レンダーループは 45fps 前後になる。** 録画自体は各アングル 1,200 フレーム以上と実用範囲だが、60fps は出ない。
## 計測 16 アングル(1080p60、CPU エンコード)でフレーム内訳を取ったところ、 `present_unit_buses` が **2 面しか描画していないのに 7.6〜8.6ms** かかっていた。 フレーム全体は 15.3〜23.0ms(予算 16.7ms)。 `present_unit_buses` を無効化すると **38.9 → 44.2 fps** に改善したことから、 present 経路そのものが原因と確定した。 ## 原因 1 フレームで **3 回の `device.submit()`** が走っていた。 1. フレーム本体(シーン合成) 2. `present_unit_buses` 3. `present_monitors` 各サブミットは共有キューで前のサブミットの完了を待つため、 後ろの 2 回が待ち時間を発生させていた。 `present_pass` と `present_monitor_pass` がそれぞれ自前の `CommandEncoder` を作って submit していたのが直接の原因。 ## 修正 - `present_unit_buses` / `present_monitors` / `present_pass` / `present_monitor_pass` / `present_monitor_group` が、 呼び出し側の `CommandEncoder` を受け取り、確保したスワップチェーン画像を返すように変更 - `submit_presents` を `present_acquired` に変え、サブミットせず提示だけを行う - レンダーループは present 用エンコーダを1本だけ持ち、両方の present パスがそこに記録する - エンコーダは**初回使用時に生成**し、記録が無ければサブミットしない (何も提示しないセッションで空のサブミットを毎フレーム投げないため) ## 検証 16 アングル、各 3 回測定: | | run1 | run2 | run3 | 平均 | |---|---|---|---|---| | 変更前 | 39.5 | 45.3 | 42.8 | **42.5** | | 変更後 | 47.7 | 43.5 | 45.0 | **45.4** | ばらつきは大きいが改善傾向。4 アングル GPU は **59.6fps / 各 960 フレーム**を維持し回帰なし。 ## 補足 `replay_playout` は 301 秒でタイムアウトするが、**この変更を外しても同じく 301 秒で失敗**する ことを確認済み(以前から CI で断続的に落ちていたテストと同じ)。 変更が原因ではない。
## 症状
録画中にデッキを開くと、アングルタイルが 1 枚もライブにならない。
16 アングル構成でもモニターとして登録されるのはデッキのライブ/出力の 2 枚だけだった
(`monitor attach id=1001`、`id=1002` のみで実測)。
## 原因
`BuildAngles` はアングルごとに `SwapchainHost` を作るが、`_angleHosts` に追加するのは
`_transport.Live` が真のときだけだった。
```csharp
AngleGridPanel.Children.Add(tile);
if (_transport.Live)
_angleHosts.Add(surface);
```
`_angleHosts` は `ReleaseNative()` がネイティブ資源を解放するために歩くリスト。
録画中は Live が偽なので、16 個のサーフェスが
- モニターにアタッチされず(`Loaded` が発火しないため `attach_monitor` に届かない)
- `_angleHosts` にも入らないため解放されない
という状態で残る。タイル再構築のたびにこれが積み上がる。
さらに、表示に使われないサーフェスを作る意味もなかった。Live でないタイルは
プロキシ静止画を描くので、そこでサーフェスは不要。
## 修正
- サーフェスは **Live のとき、または表示できるプロキシが無いとき**だけ作る
- 作ったサーフェスは Live かどうかに関わらず必ず `_angleHosts` に入れる
## 検証(実機)
| 構成 | 結果 |
|---|---|
| 16 アングル CPU | 60.1 fps/アングル、モニター attach 2 件のまま |
| 4 アングル GPU | 59.7 fps、各 960 フレーム |
録画性能に回帰なし。不要なサーフェス生成も止まった。
## 補足
16 アングル構成の録画は、この修正の前から既に 60.1 fps/アングル・欠落ゼロで動作していた。
デッキの FPS 表示が 43〜54 を示していたのは `durationFrames`(マスタークロック)を
見ているためで、描画や録画のレートではなかった。
## 症状 録画中にマルチビューを開いても、アングルタイルが動かない。 `_transport.Live` が偽のとき `BuildAngles` はプロキシ静止画に落ちるため、 カット判断に必要な「動く絵」が出ていなかった。 さらに、`Live` が偽のとき作られた `SwapchainHost` は `_angleHosts` に入らず、 `ReleaseNative()` で解放されないままタイル再構築のたびに積み上がっていた。 ## 修正 - タイルは常にライブモニター(`RetargetMonitor`)にする - 作ったサーフェスは `Live` かどうかに関わらず必ず `_angleHosts` に入れる - 提示間隔は固定値ではなく `ResolvedReplayPresentInterval()` から取る。 リプレイ設定画面の「デッキのリフレッシュ」と同じ値で、既定は 1/2 - 設定変更時にタイルにも反映する(`ApplyDeckPresentInterval`) スワップチェーン数はメイン設定画面の `FlipSwapchainLimit` がそのまま上限になる (`FlipBudget.Configure` が既に共有している)。 ## 検証 上限 8 のセッションでマルチビューを開くと 6 枚がアタッチされ、 デバイスロストは発生しなかった。 上限 16 では 16 枚のアタッチで `GPU device lost (Unknown)` が発生することを確認済み (この制約が `FlipBudget` の存在理由)。
## 症状 マルチビューを開いても、16 アングルのタイルが表示されなかった。 タイルごとに独立したスワップチェーンを作る実装で、DX12 では 16 枚で `GPU device lost (Unknown)` が発生するため、`FlipBudget` の上限(既定 6)で 大半がアタッチされずに終わっていた。 ## 原因(2つ) 1. **スワップチェーン数の上限**。タイル 1 枚 = スワップチェーン 1 枚なので、 アングル数がそのまま同時フリップ数になる。DX12 では 16 枚でデバイスロスト。 2. **`attached_monitor_sources_due` を描画対象の判定に使っていた**。 提示間隔(既定 1/2)でフィルタされるため、間隔外のフレームでは シーンが合成されず、表面は前回の内容か空になる。 合成と提示は別の問い合わせなのに、同じ条件で扱われていた。 ## 修正 - グリッドを **1 つのシーン**として構成し、**1 枚のモニター**で監視する (`Dialogs/ReplayMultiview.cs` 新規)。16 アングルでもスワップチェーンは 1 枚 - レイヤー rect は **0..1 の正規化座標**を使う(`MultiviewPane` と同じ空間)。 ピクセルで指定していたため、全タイルが画面外に描かれ黒くなっていた - 列数は 16:9 に近いセルになるよう選ぶ(正方形セルは全タイルをレターボックスにする) - 描画は既存の `MixerApply.PushLayers` を `PushSceneLayers` として公開して流用。 `draw_mv_label_pass` / `draw_mv_tally_pass` などマルチビューの描画もそのまま使う - `collect_frame_live_ids` には **アタッチ中の全モニター**を渡す(`_due` ではなく)。 提示は各モニターの間隔どおりだが、合成は毎フレーム必要 - クリックでのアングル選択は、グリッド上の位置をグリッド寸法で割って求める ## 検証(実機) | 項目 | DX12 | Vulkan | |---|---|---| | 16 アングルでグリッド表示 | 表示される | 表示される | | スワップチェーン枚数 | 3(デッキ 2 + グリッド 1) | 3 | | デバイスロスト | なし | なし | | 16 アングル録画 | 60.1 fps/アングル | — | | 4 アングル GPU 録画 | 59.6 fps・各 960 フレーム | — | | 終了 | 正常 | 正常(ハングなし) | スクリーンショットで 16 分割のグリッド表示(色・バー・ラベル)を確認済み。 ## 補足 Vulkan では `FlipBudget` の上限が効かず、16 枚のモニターを同時にアタッチしても デバイスロストしなかった。スワップチェーンの上限は DX12 側の制約で、 Vulkan には同じ制約がないことを実測で確認している。
## 症状 マルチビューでアングルをクリックしても選択が変わらなかった。 ## 原因(2つ) 1. **クリックが届いていなかった**。グリッドは `HwndHost` で子ウィンドウを持つため、 周囲の WPF 要素(Canvas)の `MouseLeftButtonUp` は発火しない。 座標計算も Canvas 基準なのにグリッド寸法で割っており、仮に届いても 16:9 以外の幅では別のタイルを指していた。 2. **選択状態を読む手段が無かった**。アクティブなアングルは太字と緑の枠だけで示され、 スクリーンリーダーにも自動テストにも伝わらない。デッキで最も使う 「これからマークするカメラを選ぶ」操作が、検証も自動操作もできなかった。 ## 修正 - クリックは `SwapchainHost.SurfaceClickedAt`(サーフェス自身の座標系で 0..1 に正規化)から受ける。 グリッドはパネル内で中央寄せなので、正規化座標なら補正が要らない - 各アングルボタンに選択状態を持たせる(`AutomationProperties.SetItemStatus` と `SetHelpText`)。 選択中は「このアングルが選択されています。」、それ以外は「押すとこのアングルを選択します。」 ## 検証(実機) 16 アングル(6 列 × 3 行)で、グリッド右側のセルをクリック: ``` selected before click: AngleTile1 clicking 690,229 selected after click: AngleTile5 RESULT: PASS ``` 座標 512/640 = 0.80 から 4 列目 → アングル 5 が選択され、期待どおり。 なお最終行は 4 セルしか埋まらないため、その右側(空セル)をクリックしても 選択は変わらないのが正しい挙動。最初はそこをクリックしていて「動かない」と 誤認しかけたので、テストの座標を実在するセルに合わせている。
## 症状 クリップを再生しているのに、デッキの状態行が「停止」のまま変わらなかった。 再生位置(playhead)は `00:00:13:02` → `00:00:16:09` と進んでおり、 再生自体は正常に動作していた。 ## 原因 状態行は録画状態(`MixerStats.State`)だけを表示していた。 再生状態は「再生/一時停止」ボタンの表示にしか反映されておらず、 状態行には一切出ていなかった。 録画と再生は別の問い合わせであり、オペレーターには両方が必要: - 「録画中 再生中」= ライブショーでプレイリストを構築中 - 「停止 再生中」= 既に録り終えた素材のレビュー ## 修正 状態行に再生状態を併記する。 - ライブ端にいる → 「ライブ」 - 記録済み素材を再生中 → 「再生中」 - 停止 → 「停止中」 ## 検証(実機) ``` t1: state=[停止 再生中] playhead=[00:00:13:02] t2: state=[停止 再生中] playhead=[00:00:13:50] t3: state=[停止 再生中] playhead=[00:00:14:35] t4: state=[停止 再生中] playhead=[00:00:15:22] t5: state=[停止 再生中] playhead=[00:00:16:09] t6: state=[停止 停止中] playhead=[00:00:16:32] ← クリップ終端 LIVE を押す → state=[停止 ライブ] ``` 再生の進行、クリップ終端での停止、LIVE 復帰がすべて正しく表示される。
## 症状 リプレイの録画フォーマットを 60/1(または 30/1)にすると、録画が始まらない。 RecordButton は押せて状態は「待機」から「停止」に変わるが、 - 映像セグメントが 1 つも書かれない(音声が数フレーム分だけ残る) - エラーは何も表示されない ## 原因 マスターのフレームレートと録画フォーマットが食い違うと、 録画開始直後の `note_fps` が不一致を検出して `stop(REPLAY_STOP_FPS_CHANGED)` を呼ぶ。 これは正しい保護(1 つの録画は 1 つのレートでしか数えられない)だが、 **停止した理由がどこにも出ない**ためオペレーターは「録画できない」としか分からない。 実測(720p60 のセッション、マスターは 60000/1001): ``` INFO ENGINE record: store opened INFO ENGINE record: lock taken, setting recording INFO NOTE_FPS master 60000x1001 vs replay 60x1 recording=true ERROR NOTE_FPS stopping: replay 60x1 != master 60000x1001 ``` ストアは開き、録画フラグも立ち、その直後に止まる。フレームは 1 枚も供給されない (`offer_frame` の呼び出しが 0 件)。 ## 修正 録画開始時の検証に、フレームレートの一致を加える。食い違えば両方のレートを名指しして断る。 ``` 録画を開始できませんでした。 replay records at 60/1 fps but the show runs at 60000/1001; set the Replay format to the show's rate ``` ## 検証(実機) - 720p60 のセッション: 上記の理由が状態行に出る(修正前は無言で停止) - 1080p 59.94 のセッション: 従来どおり録画・再生・書き出しが動作
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
Issue #4 のインスタントリプレイを、セッション / コア(Rust + wgpu)/ Windows ホスト(WPF)/ macOS ホスト(SwiftUI)/ ドキュメントまで通しで実装しました。運用に必要な「録画 → マーク → プレイリスト → 送出(映像・音声)→ 書き出し」と、vMix 互換 API・シーンアニメーションからのキューまでを含みます。
そのうえで実ソフトウェアで操作して見つけた不具合を直し、UI を作り直し、性能を測って改善しました。この PR は次の Issue も閉じます。
Closes #289
Closes #285
主な変更
セッション / コア
ReplayDto(保存先・画質・区間フレーム・エンコードキュー・書き込みバッファ・保持方針・警告しきい値・角度・イベント・プレイリスト・チャンネル)を追加。リプレイ入力がある間だけ存在し、既定セッションには含まれません。A+B−D)と Additive(合計A+D+B)を分離。Cut / Dissolve / Dip / Wipe / Slide / Push の 6 種を実際に合成します。offer_frameは 1 フレームにつき同じミューテックスを 3 回取っていたのを 2 回に、チャンネルのtickは隣り合うスナップショット 2 回を 1 回にまとめました。書き出し(実機で検証しながら修正)
ffprobe/ffmpegで実測。audioAngleIdを省略し、モジュールが「無い=無音」と解釈していました。Windows ホスト
性能(実測にもとづく改善)
1080p60、CPU エンコード、レンダーループ 60fps 維持での録画スループット:
効いたのは 3 点で、いずれも「無駄な仕事を消す」ものでした。
Vecを&[u8]で渡し、受け側でto_vec()していました。Pictureが所有するようにしました。vec![0u8; n]が 4.1MB 書いてから同じ領域をコピーしていました。未初期化で確保してcopy_nonoverlappingで埋めます。Test Plan
ローカル(Windows / DX12 / RTX 5060)で以下を実行済みです。
cargo test --workspace --all-targets(debug): mixer / control / pipeline / api / pro-api / headless / 統合リプレイ 8 本すべて成功。cargo fmt/cargo clippy -- -D warnings: 成功。dotnet build eiviz.slnx -c Release: 0 errors。npm run build成功。実ソフトウェアでの確認
ffprobeに H.264 + AAC が出て、ffmpeg -af volumedetectが無音でないことを確認。SendMessageTimeoutで測定し、0.7〜12ms を確認。DeckLink(#285)
vendor/decklink/に同梱しました(バイナリは含めていません)。DECKLINK_SDK_DIRをそのディレクトリに向け、ビルド後にdecklink_hardwareの cfg を確認します。スタブならリリースを失敗させます。備考