using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TIASshot {
public partial class PreviewMonitor : Form {
private Point _location;
private Size _size = new Size(0, 0);
private Bitmap _dispBuf;
private bool _isUpdateBuf = false;
private readonly object _dispBufLock = new object();
// 副モニタ(セカンダリ・非アクティブ・ボーダーレスウィンドウ)は,コールバックスレッドから
// 毎フレーム Invalidate() すると WM_PAINT が飢餓状態になり sub_paint_fps が数fpsまで落ち込む
// (実描画コスト・GDI 枯渇・画像サイズ肥大は計測により否定済み)。
// そのため UI タイマーで一定周期(約30fps)に自ペース再描画し,コールバックスレッドからは
// 「最新フレームの保持」のみを行い Invalidate は一切呼ばない。
private const int RefreshIntervalMs = 33; // ≒30fps
private readonly System.Windows.Forms.Timer _refreshTimer;
public bool IsShown => (_size.Width > 0);
/// <summary>
/// プレビューウィンドウのコンストラクタ
/// </summary>
public PreviewMonitor() {
InitializeComponent();
// プレビューウィンドウの位置とサイズを取得
foreach (var scr in Screen.AllScreens) {
if (scr.Primary) continue; // プライマリスクリーンは除外
if (scr.Bounds.Width <= Config.GetInt("Shot/PreviewMonitorWidth")) {
_location = scr.Bounds.Location;
_size = scr.Bounds.Size;
}
}
_refreshTimer = new System.Windows.Forms.Timer();
_refreshTimer.Interval = RefreshIntervalMs;
_refreshTimer.Tick += RefreshTimer_Tick;
this.FormClosing += (s, e) => _refreshTimer.Stop();
}
/// <summary>
/// プレビューウィンドウのロードイベント
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PreviewMonitor_Load(object sender, EventArgs e) {
// プレビューウィンドウの位置とサイズを設定
Location = _location;
Size = _size;
if (IsShown) _refreshTimer.Start();
}
/// <summary>
/// プレビューウィンドウの表示状態を更新(カメラのコールバックスレッドから毎フレーム呼ばれる)
/// 最新フレームの保持のみを行い,UI 再描画のトリガー(Invalidate)は呼ばない。
/// 実際の表示更新は RefreshTimer_Tick(UI スレッド)が自ペースで行う。
/// </summary>
/// <param name="image"></param>
public void UpdateImage(Bitmap image) {
#if MEMMONITOR
PreviewProfiler.IncrementSubUpdates();
#endif
// 原因3: RefreshTimer_Tick に反映されなかった旧バッファを上書き前に Dispose する
// _dispBufLock で UpdateImage(コールバックスレッド)と RefreshTimer_Tick(UIスレッド)の
// 競合・二重 Dispose を防ぐ
Bitmap toDispose = null;
lock (_dispBufLock) {
if (_isUpdateBuf && _dispBuf != null) {
toDispose = _dispBuf;
}
_dispBuf = image;
_isUpdateBuf = true;
}
#if MEMMONITOR
if (toDispose != null) PreviewProfiler.IncrementSubDrops();
#endif
toDispose?.Dispose(); // ロック外で GDI 解放(コールバックスレッド上だが,まだ表示に使われていないフレームのみ)
// Invalidate() は呼ばない(毎フレーム呼ぶと WM_PAINT が飢餓状態になるため)。
}
/// <summary>
/// UI タイマーによる自ペース再描画(約30fps)。
/// 新フレームがあるときだけ picPreview.Image を差し替える(静止フレームは無駄に再描画しない)。
/// </summary>
private void RefreshTimer_Tick(object sender, EventArgs e) {
if (!IsShown) return;
// 原因3: _dispBufLock で UpdateImage との競合を防ぎながらバッファを取り出す
Bitmap toDisplay;
lock (_dispBufLock) {
if (!_isUpdateBuf || _dispBuf == null) return;
toDisplay = _dispBuf;
_dispBuf = null; // 参照を手放し、UpdateImage 側が誤 Dispose しないようにする
_isUpdateBuf = false;
}
// picPreview.Image の Dispose・差し替えは UI スレッド(このタイマー Tick)でのみ行う。
// 代入によって picPreview 自身が再描画され,MEMMONITOR の TimedPictureBox.OnPaint 計測も
// 引き続き機能する。
picPreview.Image?.Dispose(); // 直前に表示していたバッファを解放
picPreview.Image = toDisplay;
#if MEMMONITOR
PreviewProfiler.IncrementSubPaints();
#endif
}
}
}