Captain America vs VirtualSurfaceImageSource


はじめに


ほとんどの場合、Windowsランタイムの開発は比類のない喜びをもたらします。 まったく何もありません。コントロールを構築し、MVVMを少し追加してから、あなたは座ってコードを賞賛します。 これは、99%のケースで発生します。 残りの100分の1で、タンバリンとの本物のダンスが始まります。

実際、私は誇張して、完全に絶望的な状況でのみ異教の儀式に頼っています。 しかし、WP開発者には、少なくともすべての不幸を説明した貧しいSilverlight開発者から始めて、MSを非難するものがあります。 まあ、大丈夫、それはすべてトピック外になりました。

キャップ、どこにいるの?


だから、仮想的な状況に精神的に早送りします。 アプリケーションがあり、それをWindows 8.1のkinopoisk.ruのクライアントにしましょう。 そして、数百万ドルの予算と私たちの好きな漫画のスーパーヒーローがいるハリウッドAAAプロジェクトのポスター。 タスクは、ユーザーに完璧な品質でポスターを表示することです。 「理想」という言葉は、 1ピクセルの画像== 1ピクセルの物理的な対応を意味します。

それは些細なことのように見え、 画像を作成して、それをSourceプロパティに、画像とともに希望のBitmapImageに割り当てます。 しかし、画像サイズは驚くべきものです-9300 x12300。電卓を手に入れて、カウントを開始します:9300 * 12300ピクセル* 4 B /ピクセル= 436 MB。 かなり印象的な人物ですが、21世紀にはそのようなことに驚くことはありません。 2010年の平均的なデスクトップは、このような量のデータを簡単に消化するため、F5キーを押して作成を楽しんでいます。 少なくとも私のコンピューターでは、すべて正常に動作します。 この記事は終了する可能性があります...

出入り口全体が狭すぎたとき


それでは、TKを精神的に修正します。 クライアントkinopoisk.ruを新しい「ユニバーサルアプリケーション」、つまり WindowsとWindows Phoneの両方に対応する1つのアプリケーション。 さて、コードを編集する必要はなく、再コンパイルするだけで十分でした。 袖をまくり上げて、Lumia 920を起動すると...すぐに落ちます...

少しグーグルで調べたところ、私のlumiya(1 GBのメモリを搭載)では、アプリケーションに利用可能な390 MBしかなく、 これは明らかに私たちの写真には収まりません 。 私の場合、2 GBのデバイスは依然として許容できません。 残念ながら、回避策を探す必要があります。

そして今、私は何をすべきですか?


あなたが言う。 一般に、いくつかのオプションがあります。
  1. 画像を許容可能なサイズに縮小する
  2. 画面に表示されている部分のみを描画します

最初のオプションはすぐに消え、品質を犠牲にしません。 2番目にまっすぐ進みます。 ここでも、交差点が待っています。
  1. すべてを自分で書く
  2. VirtualSurfaceImageSource

そして、実際のプログラマーは主に怠zyなプログラマーであることを思い出しました。 そのため、レドモンドの開発者から親切に提供された既製のバージョンを使用します。 実際、 VirtualSurfaceImageSourceには、XAML + C#バンドルではそれ自体では達成できなかったいくつかの利点がありますが、これらの利点はすべて後ほど提供します。

VirtualSurfaceImageSource-同じ「銀の弾丸」


それで、ここで今日のプログラムの「爪」に行きます。 前述したように、 VirtualSurfaceImageSourceは画像の可視部分のみをメモリに保存します。 アプリケーションが大量のデータを表示する必要がある場合、これは非常に役立ちます。 私たちは常にそのようなアプリケーションに直面しています:マップ(Bing Maps、こちらのマップ)、PDFリーダー(Windows 8.1にあります)、そしてWindows PhoneでInternet Explorer、Word、Excelなどのクールなアプリケーションでさえ、同様のテクノロジーを使用しています。

説明はかなり単純化されましたが、実際にはVirtualSurfaceImageSourceのロジックはるかに複雑であり、 内部では多くの計算とあらゆる種類の最適化を実行します。 これらの詳細は私たちに関係するべきではありません、これはすべて退屈で面白くないです。 重要なのは、外部から私たちに見えるものです。


そして、私たちにとって、すべては非常に簡単です。 VirtualSurfaceImageSourceは、画像のどの部分を再描画するかについてのガイダンスを提供します。 彼は残りの仕事を引き受けます。 上の図に示されているように、画像の可視部分のみを描画します。 オフセット座標を読み取る必要はありません。VirtualSurfaceImageSourceがそれらを計算します。 大まかに言えば、私たちの行動の順序は次のとおりです。

  1. IVirtualSurfaceImageSourceNativeを取得する
  2. RegisterForUpdatesNeededを使用したレンダリングのサブスクライブ
  3. コールバックを呼び出すとき、目的の領域を描画します


免責事項!
そして、はい、私はほとんど警告するのを忘れていました-ここにC#はありません! はい、そのような場合は、快適ゾーンを離れる必要があります。 ただし、急いでタブを閉じないでください。記事の重要な部分はWin2Dにも適用できます。 VirtualSurfaceImageSourceのラッパーはロードマップにすでにリストされているため、少し時間がかかります。 または、実装でプルリクエストを行うことができます。 近い将来、これを行う予定ですので、ご期待ください!


すべてが非常にシンプルに見えますが、コードを記述するだけです。 Direct2Dを使用して描画しますが、私の場合はSurfaceのメモリの平凡なコピーが適切です。 多数の不必要なプロジェクトでソリューションを乱雑にしないために、C ++ Blank App(Universal Application)を作成しました。 C ++ / CXの出現により、C#コードとの相互作用が最小限の変更にまで削減されたため、この記事ではこのトピックを巧みに回避します 。 しかし、突然誰かが興味を持ったら、コメントを書いてください、私は喜んであなたに話します!

ステップ0:準備


この例でも、 C ++ Blank App(Universal Application)を作成しました。 簡単にするために、すべてのコードはMainPageページの分離コードにあります。

IVirtualSurfaceImageSourceNativeは Windowsランタイムインターフェイスでないため、特別なヘッダーファイルを含める必要があります。

#include <windows.ui.xaml.media.dxinterop.h>


必要なすべてのフィールドとメソッドを宣言します。

パブリック ref クラス MainPageの 封印
{
公開
MainPage();
void UpdatesNeeded();

プライベート
// DirectXメソッド
void CreateDeviceResources();
void CreateDeviceIndependentResources();
void HandleDeviceLost();

// VirtualSurfaceImageSourceを作成します
//そしてImageに設定します
void CreateVSIS();

//指定された領域を描画します
void RenderRegion( const RECT &updateRect);

プライベート
float dpi;
ComPtr < ID2D1Factory1 > d2dFactory;
ComPtr < ID2D1Device > d2dDevice;
ComPtr < ID2D1DeviceContext > d2dDeviceContext;
ComPtr < IDXGIDevice > dxgiDevice;

//私たちの画像
BitmapFrame ^ bitmapFrame;
//このVirtualSurfaceImageSourceへのリンク
VirtualSurfaceImageSource ^ vsis;
// IVirtualSurfaceImageSourceNativeへのリンク
ComPtr < IVirtualSurfaceImageSourceNative > vsisNative;
};


そしてコンストラクター:

MainPage :: MainPage()
{
InitializeComponent();
//現在のDPIを取得します
dpi = DisplayInformation :: GetForCurrentView()-> LogicalDpi;
CreateDeviceIndependentResources();
CreateDeviceResources();
CreateVSIS();
}


誰かがComPtr <T>によって混乱しない限り、ここでコメントする特別なものはありません。 これは、 COMオブジェクト専用のshared_ptr <T>のような通常のスマートポインターです。

将来的には、デバッグ時に非常に役立つこのような単純なものを使用します。

名前空間 DX
{
インライン void ThrowIfFailed( _In_ HRESULT hr
{
if失敗hr ))
{
//この行にブレークポイントを設定して、DX APIエラーをキャッチします。
throwプラットフォーム:: 例外 :: CreateException( hr );
}
}
}


ステップ1:初期化ルーチン


ここで興味深いことは何もありません。これをあなたの手で書くのは、フッサール以外のことです。 そのため、最小限の変更でこのコードをMSの例から引き出しました。 コメントはオリジナルです。

//デバイスに依存しないリソースを作成します
void MainPage :: CreateDeviceIndependentResources()
{
D2D1_FACTORY_OPTIONSオプション。
ZeroMemory (&options、 sizeofD2D1_FACTORY_OPTIONS ));

#if 定義済み_DEBUG
//プロジェクトがデバッグビルドにある場合、Direct2D SDKレイヤーを介してDirect2Dデバッグを有効にします。
// SDKデバッグレイヤーを有効にすると、無効な呼び出しや
//開発サイクル中に修正する必要があるリソースリーク。
options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION ;
#endif

DX :: ThrowIfFailed(
D2D1CreateFactory(
D2D1_FACTORY_TYPE_SINGLE_THREADED
__uuidofID2D1Factory1 )、
&オプション、
&d2dFactory

);
}
//これらはハードウェアに依存するリソースです。
void MainPage :: CreateDeviceResources()
{
//このフラグは、APIのデフォルトとは異なるカラーチャネルの順序を持​​つサーフェスのサポートを追加します。
//これは推奨される使用方法であり、Direct2Dとの互換性のために必要です。
UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT ;

//この配列は、このアプリがサポートするDirectXハードウェア機能レベルのセットを定義します。
//順序が保持されることに注意してください。
D3D_FEATURE_LEVEL featureLevels [] =
{
D3D_FEATURE_LEVEL_11_1
D3D_FEATURE_LEVEL_11_0
D3D_FEATURE_LEVEL_10_1
D3D_FEATURE_LEVEL_10_0
D3D_FEATURE_LEVEL_9_3
D3D_FEATURE_LEVEL_9_2
D3D_FEATURE_LEVEL_9_1
};

// D3D11 APIデバイスオブジェクトを作成し、対応するコンテキストを取得します。
ComPtr < ID3D11Device > d3dDevice;
ComPtr < ID3D11DeviceContext > d3dContext;
D3D_FEATURE_LEVEL featureLevel;
DX :: ThrowIfFailed(
D3D11CreateDevice(
nullptr//デフォルトのアダプタを使用するにはnullを指定します
D3D_DRIVER_TYPE_HARDWARE
0、 //ソフトウェアデバイスでない限り0のままにする
creationFlags、 //オプションでデバッグフラグとDirect2D互換性フラグを設定します
featureLevels、 //このアプリがサポートできる機能レベルのリスト
ARRAYSIZE (featureLevels)、 //上記リストのエントリ数
D3D11_SDK_VERSION//モダンスタイルアプリの場合は常にこれをD3D11_SDK_VERSIONに設定する
&d3dDevice、 //作成されたDirect3Dデバイスを返す
&featureLevel、 //作成されたデバイスの機能レベルを返します
&d3dContext //デバイスの即時コンテキストを返します

);

// Direct3D11.1デバイスの基礎となるDXGIデバイスを取得します。
DX :: ThrowIfFailed(
d3dDevice.As(&dxgiDevice)
);

// 2-Dレンダリング用のDirect2Dデバイスを取得します。
DX :: ThrowIfFailed(
d2dFactory-> CreateDevice(dxgiDevice.Get()、&d2dDevice)
);

//そして、対応するデバイスコンテキストオブジェクトを取得します。
DX :: ThrowIfFailed(
d2dDevice-> CreateDeviceContext(
D2D1_DEVICE_CONTEXT_OPTIONS_NONE
&d2dDeviceContext

);

//このデバイスコンテキストはXAMLサーフェスイメージソースにコンテンツを描画するために使用されるため、
//ピクセルとして動作する必要があります。 ピクセル単位モードの設定は、Direct2Dに処理を指示する方法です
//ピクセル単位など、通常はDIPとしての入力座標とベクトル。
d2dDeviceContext-> SetUnitMode( D2D1_UNIT_MODE_PIXELS );

//入力値をピクセルとして扱いますが、Direct2Dに伝えることは依然として非常に重要です
//アプリケーションが動作する論理DPI。 Direct2Dは、DPI値をヒントとして使用して
//有効にするタイミングを決定するなど、内部レンダリングポリシーを最適化します
//対称テキストレンダリングモード。 この場合、適切なDPIを指定しないと傷つきます
//アプリケーションのパフォーマンス。
d2dDeviceContext-> SetDpi(dpi、dpi);

//アプリケーションがグラフィックコンテンツのアニメーションまたは画像合成を実行するとき、それは重要です
// ClearTypeではなくDirect2Dグレースケールテキストレンダリングモードを使用します。 cleartypeテクニック
//アルファチャネルではなくカラーチャネルで動作するため、正しく実行できません
//画像の構成またはテキストのサブピクセルアニメーション。 ClearTypeは、次の場合に依然として選択の方法です。
//後続の構成を必要とせずに、テキストを目的のサーフェスに直接レンダリングします。
d2dDeviceContext-> SetTextAntialiasMode( D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE );
}


注目に値する唯一のものはSetUnitMode()です。 コメントによると、原則として、すべてが明確でなければなりません。 ただし、Direct2Dプリミティブまたはテキストを描画する場合は、値をD2D1_UNIT_MODE_DIPSに変更することを忘れないでください。 私たちの場合、これは干渉するだけです。

ステップ2:VirtualSurfaceImageSourceを作成する


この操作は、わずか3つのアクションになります。

// VirtualSurfaceImageSourceを作成します
//透明性は不要なので、isOpaque = false
vsis = ref new VirtualSurfaceImageSource (bitmapFrame-> PixelWidth、bitmapFrame-> PixelHeight、 false );

// VirtualSurfaceImageSourceをIVirtualSurfaceImageSourceNativeにキャストします
DX :: ThrowIfFailed(
reinterpret_cast < IInspectable *>(vsis)-> QueryInterface( IID_PPV_ARGS (&vsisNative))
);

// DXGIデバイスをインストールします
DX :: ThrowIfFailed(
vsisNative-> SetDevice(dxgiDevice.Get())
);


次に、コールバックオブジェクトを作成する必要があります。 これを行うには、 IVirtualSurfaceUpdatesCallbackNativeを実装する新しいクラスを宣言します。

クラス VSISCallbackpublic RuntimeClass < RuntimeClassFlags < ClassicCom >、 IVirtualSurfaceUpdatesCallbackNative >
{
公開
HRESULT RuntimeClassInitialize( _In_ WeakReference パラメーター
{
参照= パラメーター ;
return S_OK ;
}

IFACEMETHODIMPの更新が必要()
{
// MainPageにキャスト^
MainPage ^ mainPage = reference.Resolve < MainPage >();
// mainPageがまだ削除されていない場合
if (mainPage!= nullptr
{
mainPage-> UpdatesNeeded();
}
return S_OK ;
}

プライベート
WeakReferenceリファレンス。
};


領域を再描画する必要がある場合、このコールバックがトリガーされます。 実装では、 MainPage :: UpdatesNeeded()を呼び出します。これは、すべての汚い作業を行います。 WeakReferenceは 、別のページに移動したがコールバックの登録を解除し忘れた場合にメモリリークを防ぐために必要です。

このコールバックを登録するだけです。

// VSISCallBackのインスタンスを作成します
WeakReferenceパラメーター( this );
ComPtr < VSISCallback >コールバック。
DX :: ThrowIfFailed(
MakeAndInitialize < VSISCallback >(&コールバック、パラメーター)
);

//コールバックを登録します
DX :: ThrowIfFailed(
vsisNative-> RegisterForUpdatesNeeded(callback.Get())
);


ステップ3:描画


まず、すべての「ダーティ」リージョンを取得します。 その後、それぞれを描画します。

void MainPage :: UpdatesNeeded()
{
//再描画可能な領域の数を取得します
DWORD rectCount;
DX :: ThrowIfFailed(
vsisNative-> GetUpdateRectCount(&rectCount)
);

//リージョン自体を取得します
std :: unique_ptr < RECT []> updateRects( 新しい RECT [rectCount]);
DX :: ThrowIfFailed(
vsisNative-> GetUpdateRects(updateRects.get()、rectCount)
);

//それらを描く
forULONG i = 0; i <rectCount; ++ i)
{
RenderRegion(updateRects [i]);
}
}


そしてついに、私たちは待望のサガのフィナーレであるドローイングに来ました。 しかし、最初に、1つの小さな発言。


赤い長方形を現在の領域とします。 この領域の場合、 IVirtualSurfaceImageSourceNative :: BeginDraw()を呼び出すと、目的のSurfaceが得られ、その上に領域全体を赤い長方形で描画する必要があります。 図面の最後で、 IVirtualSurfaceImageSourceNative :: EndDraw()を呼び出します。

これはどういう意味ですか? そのSurfaceは現在の領域のみを表示します。 つまり、このSurfaceの原点はリージョンの左上隅にあり、余分な転送について考える必要はありません。 この地域を超えることはできません。

言葉では少し混乱するように聞こえますが、実際にはすべてが非常に明確になるので、始めましょう:

void MainPage :: RenderRegion( const RECTupdateRect
{
//サーフェス、描画する場所
ComPtr < IDXGISurface > dxgiSurface;
//表面オフセット
POINT surfaceOffset = {0};

HRESULT hr = vsisNative-> BeginDraw( updateRect 、およびdxgiSurface、およびsurfaceOffset);

if成功 (hr))
{
// Surfaceをビットマップに変換し、その上に描画します
ComPtr < ID2D1Bitmap1 > targetBitmap;
DX :: ThrowIfFailed(
d2dDeviceContext-> CreateBitmapFromDxgiSurface(
dxgiSurface.Get()、
nullptr
&targetBitmap

);
d2dDeviceContext-> SetTarget(targetBitmap.Get());

// surfaceOffsetへの転送を行います
自動変換= D2D1 :: Matrix3x2F ::翻訳(
static_cast < float >(surfaceOffset.x)、
static_cast < float >(surfaceOffset.y)
);
d2dDeviceContext-> SetTransform(transform);

//ビットマップを描画します
d2dDeviceContext-> BeginDraw();

// *********************
// TODO:ここに描く
// *********************

DX :: ThrowIfFailed(
d2dDeviceContext-> EndDraw()
);

//自分自身をきれいにします
d2dDeviceContext-> SetTarget( nullptr );

//描画を終了
DX :: ThrowIfFailed(
vsisNative-> EndDraw()
);
}
else if ((hr == DXGI_ERROR_DEVICE_REMOVED )||(hr == DXGI_ERROR_DEVICE_RESET ))
{
//デバイスのリセットを処理します
HandleDeviceLost();
// updateRectを再度描画しようとしています
vsisNative->無効化( updateRect );
}
他に
{
//不明なエラー
DX :: ThrowIfFailed(hr);
}
}


結果のsurfaceOffsetに注意してください。 すべてのアートワークをsurfaceOffsetにシフトする必要があります。 これは生産性を上げるために行われましたが、そのような微妙さを心配する必要はありません。 シフトする最も簡単な方法は、変換マトリックスです。

デバイスのリセット(ドライバーの障害など)が発生した場合、デバイスを再作成し、 IVirtualSurfaceImageSourceNative :: Invalidate()で現在の領域を「ダーティ」としてマークします。 したがって、 VirtualSurfaceImageSourceは後で領域再描画します。

キャップを支持して1-0



この例のコードはGitHubにあります。

それで、本当に難しいパスをしたので、私はついにルミヤでアプリケーションを起動し、...本当に喜びます! 悲しいかな、第一印象は常に誤解を招くものであり、この場合も例外ではありません。 スワイプするときに耐えられないラグを見て、私はとても怒っていました。 はい、目標を達成しましたが、費用はいくらですか? このようなクラフトをWindowsストアに投稿することはできません。それなしでは十分なゴミがあります。

これらの遅れの理由は、いつものように、ありふれたものです-UIストリームをブロックします。 C#アプリケーションの場合、async + awaitバンドルがほぼ常に保存される場合、この場合、非同期性に問題があります。
読者を観察すると、この投稿のタイトルに「パート1」とすぐに気が付きました。 そして、すべては私が多くのものをカバーしなかったからです。 たとえば、Trim。このアプリケーションは、Windowsストアでの認定に合格しません。 そして最も重要なのは、別のスレッドレンダリングすることです。 したがって、2つの鳥を1つの石で殺します。サンプルコードのシングルスラッシュスラッシュと、スクロールするときのひどいブレーキを取り除きます。

今日は以上です。 コーディングをもっと楽しくし、ユーザーをもっと幸せにしたいです!

Source: https://habr.com/ru/post/J242311/


All Articles