最新のマントのリフレッシュ料金は、2d あたり 60 ~ 120 フレームの範囲です。ソフトウェア プログラムに完全に到達すると、フレームごとに 8.33 ミリ秒でピクセルがクローク クロークにプッシュされます。 これには、ソフトウェア プログラムの宣言の更新、UI パーツのレイアウト、および最後にフレーム バッファーへのレコードの書き込みが含まれます
厳しい締め切りではないでしょうか。Electron でソフトウェア プログラムを構築したことがある人にとっては、そうでなくても、継続的に満たす必要があると感じることのできる締め切りです。 Atom に取り組んで、これがまさに私たちが感じた方法です。私たちがどれだけ難しいことを試みたかという問題はありませんでした。 無駄なシーケンスの結果としてのランダムな中断と、フレームを気にしませんでした。 豪華な DOM の再レイアウトと、1 つおきのフレーム以外は気にしませんでした。 フレーム料金が一度変更されて一貫性がなく、ほとんどの原因は以前の変更にありました。 しかし、単純なコンテナーとグリフで構成される Atom のレンダリング パイプラインをマイクロ最適化するのに苦労している間、2d あたり 120 フレームという一定の料金でインテリジェントで複雑なジオメトリをレンダリングする PC ビデオ ゲームにがっかりしました。
s は、かつてより強力な低速に変更されました。立体的でフォトリアリスティックなペルソナを描く?
Zed を取得するために、コード エディターを構成する必要がありました。 ゲームの世界に感銘を受けた私たちは、UI フレームワークである GPUI を取得するために一度変更したいパフォーマンスを完全に形成することができることに気付きました。
Zed はビデオゲームのようにレンダリングされます。これにより、実際の人物インターフェースのすべてのレイヤーを爆発させ、それらの周りを回転する 3D デジカメをシミュレートできます。 Zed の構築を開始したとき、GPU での任意の 2D グラフィックス レンダリングは、かつては非常に強力な研究プロジェクトに変更されました。 パトリック・ウォルトンの パスファインダー [unit_vertex_id] で実験を行いました クレートですが、パフォーマンス目標を迅速に設定するには十分ではありませんでした. そこで、私たちは一歩踏み出し、解決しようとしていたトピックを再考しました。 任意のグラフィックスをレンダリングする優れたライブラリも素晴らしいものでしたが、真実は、Zed にはそれを必要としなかったことに変わりました。 適用すると、ほとんどの 2D グラフィカル インターフェイスは、四角形、影、テキスト、アイコン、および写真のほぼ全体のパーツに分解されます。 全体的な原因のグラフィックス ライブラリへの配慮の変更として、Zed の UI をレンダリングする必要があるとわかっている特定のグラフィカル ベテランごとにカスタム シェーダーを作成することに焦点を当てることにしました。 すべてのベテランのプロパティを CPU に記録することで、重労働のすべてを GPU に任せることができ、ビルド UI パーツは間違いなく並行して描画されます。 以下のセクションでは、すべての退役軍人をプロットするために GPUI で消滅する方法を説明します.
スタイリッシュまたはガーデンの長方形は、グラフィカル UI の基本的な構成要素です。
GPUI での長方形の描画方法を大切にするために、最初に Signed Distance Capabilities (一時的な SDF) の信念への迂回を消費したいと考えています。 名前が示すように、SDF は、入力状況が与えられると、数学的に定義されたオブジェクトの周囲までの距離を返す特性です。 状況がオブジェクトに近づくため、距離はゼロに近づき、その境界内に足を踏み入れると敵対的になります。
円の符号付き距離特性.
知られている SDF のリストは、主に Inigo Quilez の 精力的な作業 話題になっている。 さらに、彼の Web ページでは、SDF の歪み、構成、反復によって、本質的に最も複雑で合理的な 3D シーンを生成する方法について、終わりのない一連の記事を見つけることができます。 まじで勉強しろ[[stage_in] 。
四角形のサポート: がんばりましょう彼らのための自衛隊の。 プロットしたい四角形を原点に置くことで、トピックを単純化することができます。 ここから、または対称であるトピックを探すのはやや簡単です。 つまり、四象限のいずれかにある範囲の距離を計算することは、三象限のいずれかにあるその点の複製画像の距離を計算することと同じです。
原点に四角形を描くことで、関連する価格を完全に活用し、特定のものに関して完全に懸念することができます四分円。
これは、四角形の頭に適した部分について完全に把握したいことを意味します。 コーナーを参考にすると、次の 3 つの条件を区別できます。
ケース 1)、点は角の上と左の両方にあります。 この場合、点と長方形の間の最短距離は、マントから頭の端までの垂直距離によって与えられます。 ケース 2)、ポイントは下とコーナーのベストの 1 つにある。 この場合、点と長方形の間の最短距離は、ベストエッジのマント 1 つからの水平距離によって与えられます。 ケース 3)、ポイントが上にあり、コーナーのベストの 1 つに。 この場合、ピタゴラスの定理を利用して角と点の間の距離を見つけることができます。
ケース 3 は、敵対的な部分を取り除くために距離ベクトルを禁止する場合、約 2 を覆うように一般化することもできます.
上記 3) の場合、コーナーから同じ距離に無限に多くの機能が配置されていると考えてください。 実は、それらは素晴らしいランダム機能ではなく、角から始まり距離に等しい半径を持つ円を争う機能です.
まっすぐな長方形から離れるにつれて、よりスムーズにコツをつかむために国境が開いています。 これは、丸みを帯びた角を描画する上で最も価値のある洞察です。希望する角の半径が与えられると、それによって新しい長方形を縮小し、点までの距離を計算し、計算された距離から角の半径を差し引くことができます。
四角形の SDF を GPU に移植することは、驚くほど直感的です。 簡単に要約すると、従来の GPU パイプラインには、頂点シェーダーとフラグメント シェーダーが含まれています
頂点シェーダーは、任意の入力レコードを 3D コンドミニアムの機能にマッピングする責任があります。3 つの機能のすべての状況で、マント マントにプロットしたい三角形を定義します。 次に、頂点シェーダーによって生成された三角形内のすべてのピクセルに対して、GPU はフラグメント シェーダーを呼び出します。これは、特定のピクセルに配色を割り当てる責任があります。
私たちのケースでは、頂点シェーダーを利用して、マントにプロットしたい形状の境界フィールドを解釈します 2 つの三角形の利用. また、このフィールド内のすべてのピクセルを必ずしも占有する必要はありません。 それは次にお話しするフラグメントシェーダーに任せます
次のコードは Steel Shader Language であり、 で消滅するように設計されています) インスタンス化されたレンダリング をプロットする1 つのプロット名でマント マントに四角形の合計ロット:
構造体 RectangleFragmentInput {
ドリフト rect_sdf[[buffer(GPUIRectInputIndexVertices)]
( float2 絶対ピクセル位置, float2 原点, float2 次元,
ドリフト[[stage_in] コーナー半径 )
{ float2 half_size=寸法 / 2. ; float2 rect_center=原点 + half_size; float2 pixel_position=abs(absolute_pixel_position – rect_center) ; float2 shrunk_corner_position=half_size – corner_radius; float2 pixel_to_shrunk_corner=max(float2( 0. 、 0.), pixel_position – shrunk_corner_position) ; ドリフト distance_to_shrunk_corner=寸法 (pixel_to_shrunk_corner); ドリフト 距離=distance_to_shrunk_corner – corner_radius; 戻る 距離; } フラグメント float4 rect_fragment(RectangleFragmentInput 入力 [[buffer(GPUIRectInputIndexUniforms)]]) { ドリフト 距離=rect_sdf( 入力.シチュエーション.xy、input.origin、input.dimension、input.corner_radius、); もしも (距離> 0.0)) { 戻る float4(
0.、
タンブル シャドウをレンダリングするにはGPUI では
の共同創設者であるエヴァン・ウォレスによって開発されました。フィグマ。 完全を期すために、ここにブログ投稿の内容を要約します。 ガウスぼかしを使用して、意図的にタンブル シャドウを着実にレンダリングします。 すべての出力ピクセルについて、ガウスぼかしは、周囲のすべての入力ピクセルの重み付けされた中程度の結果であり、すべてのピクセルに割り当てられた重みは、ガウス曲線に従うリーチ内の遠いピクセルに対して減少します.
Zed トレースにガウスぼかしを適用しています。
力の領域に航海すれば、上記の式は、入力スタンプ (個別のケースでは、論争のピクセル) と の畳み込みであるためです。 ガウス特性
ガウスぼかしの興味深い側面の 1 つは、それらが分離可能であることです。 つまり、ぼかしはさらに x 軸と y 軸に沿って個別に適用することもでき、結果の出力ピクセルは、2 次元で単一のぼかしを適用するのと同じになります。
長方形の場合、隣接するピクセルをサンプリングせずにぼやけたバージョンをプロットするためのクローズド フェッチ解像度が存在します。 これは、長方形がさらに分離可能であり、2 つの交差 有蓋車の機能
[unit_vertex_id] ステップ特性を持つガウスの畳み込みは、ガウスの積分に相当し、エラー特性
(また erf[[buffer(GPUIRectInputIndexVertices)] ))。 その後、ぼやけた直線の長方形を生成することは、すべての次元を個別にぼかすことと同じであり、2 つの結果を交差させます:
ドリフト
(float2 pixel_position, float2 origin, float2 dimension,ドリフト シグマ)
{ float2 bottom_right=原点 + 寸法; float2 x_distance=float2(pixel_position.x – origin.x, pixel_position.x – bottom_right.x); float2 y_distance=float2(pixel_position.y – origin.y, pixel_position.y – bottom_right.y); float2 integral_x=0.5 + 0.5[[stage_in] erf(x_distance ([[stage_in] sqrt[unit_vertex_id] (0.5
)/シグマ)); float2 integral_y=
0.5 + 0.5[[stage_in] erf(y_distance ([[stage_in] sqrt[unit_vertex_id] (0.5
)/シグマ)); 戻る (integral_x.x – integral_x.y ) (積分_y.x – 積分_y.y); } [[stage_in] それにもかかわらず、上記のようなクローズド フェッチ解像度は、ガウスを使用した角丸四角形の 2D 畳み込みには存在しません。これは、角丸四角形の式が分離できないためです。 Evan Wallace の近似の巧妙さは、1 つの軸に沿ってクローズド フェッチの堅実な畳み込みを実行し、次に逆軸に沿って有限量のケースでガウス分布を手動でスライドさせることに由来します:
ドリフト
(
ドリフト
バツ、 ドリフト y, ドリフト
シグマ、ドリフト[[buffer(GPUIRectInputIndexVertices)] コーナー、float2 ハーフサイズ)
{ ドリフト
デルタ=分 (half_size.y – コーナー – abs
コーナー半径、
ドリフト シグマ)
{ float2 half_size=寸法 / 2.; float2 ハート=原点 + ハーフサイズ; float2 point=pixel_position – ハート; ドリフト low=point.y – half_size. y; ドリフト 高=point.y + half_size. y; ドリフト オープン=クランプ(-3. シグマ、低、高); ドリフト アトマイズ=クランプ(3.[[stage_in] シグマ、低、高); ドリフト step=(噴霧 – 開く) / 4. ; ドリフト y=オープン + ステップ0.5[[stage_in] ; ドリフト アルファ=0.; ために ([[buffer(GPUIGlyphVertexInputIndexVertices)] int[unit_vertex_id] i=[unit_vertex_id] 0 ; 私 <
4 ; i++) { alpha +=blur_along_x(point.x, point.y - y, sigma, corner_radius, half_size) ガウス(y, シグマ) ステップ; y +=ステップ; } 戻る アルファ; } [[stage_in] テキストレンダリング
グリフを効率的にレンダリングすることは、Zed のようなテキスト集約型のアプリケーションにとって非常に重要です。 同時に、ターゲット オペレーティング システムのルック アンド フィールに一致するテキストを生成することも同様に重要です。 GPUI で両方の問題をどのように解決したかを理解するには、 テキストの整形とフォントのラスタライズがどのように機能するかを理解する必要があります.
テキスト整形 とは、どのグリフを整形するかを決定するプロセスを指します。文字列とフォントが指定された場合にレンダリングされる場所と配置する場所を指定します。 がある いくつかの オープンソース 整形
エンジン
、およびオペレーティング システムは通常、すぐに使用できる同様の API を提供します (たとえば、macOS の CoreText)。 一般に、シェーピングは非常にコストがかかると見なされており、本質的に難しい アラビア語やデーバナーガリーなどのタイプセットに変換します。
0.5[[stage_in] ; ドリフト アルファ=0.; ために ([[buffer(GPUIGlyphVertexInputIndexVertices)] int[unit_vertex_id] i=[unit_vertex_id] 0; 私 <
4
; i++) { alpha +=blur_along_x(point.x, point.y - y, sigma, corner_radius, half_size) ガウス(y, シグマ) ステップ; y +=ステップ; } 戻る アルファ; } [[stage_in] テキストレンダリング
グリフを効率的にレンダリングすることは、Zed のようなテキスト集約型のアプリケーションにとって非常に重要です。 同時に、ターゲット オペレーティング システムのルック アンド フィールに一致するテキストを生成することも同様に重要です。 GPUI で両方の問題をどのように解決したかを理解するには、 テキストの整形とフォントのラスタライズがどのように機能するかを理解する必要があります.
テキスト整形 とは、どのグリフを整形するかを決定するプロセスを指します。文字列とフォントが指定された場合にレンダリングされる場所と配置する場所を指定します。 がある いくつかの オープンソース 整形
エンジン
、およびオペレーティング システムは通常、すぐに使用できる同様の API を提供します (たとえば、macOS の CoreText)。 一般に、シェーピングは非常にコストがかかると見なされており、本質的に難しい アラビア語やデーバナーガリーなどのタイプセットに変換します。
この問題に関する重要な観察結果の 1 つは、通常、テキストはフレーム間であまり変化しないということです。 たとえば、コードの行を編集しても周囲の行には影響しないため、それらを再形成するのに不必要にコストがかかります.
そのため、GPUI はオペレーティング システムの API を使用して整形を実行し (これにより、テキストが他のネイティブ アプリケーションと一貫して表示されることが保証されます)、整形されたグリフに対するテキストとフォントのペアのキャッシュを維持します。 . テキストの一部が初めて整形されると、キャッシュに挿入されます。 後続のフレームに同じテキストとフォントのペアが含まれている場合、整形されたグリフが再利用されます。 逆に、テキストとフォントのペアが後続のフレームから消えると、キャッシュから削除されます。 これにより、シェーピングのコストが償却され、あるフレームから別のフレームに変更されるテキストのみに制限されます.
フォントのラスタライズ 一方、グリフのベクトル表現をピクセルに変換するプロセスを指します。 ラスタライザを実装するにはいくつかの方法があり、オペレーティング システム (macOS の CoreText など) や
によって提供される従来の CPU ラスタライザを使用しますFreeType[[buffer(GPUIRectInputIndexVertices)] 、最近のいくつかの研究プロジェクトでは、主に GPU を使用して [[stage_in] を使用しています。 計算シェーダー (例: パスファインダー、 フォーマ 、 また ヴェロ
).
[[buffer(GPUIGlyphVertexInputIndexVertices)] しかし、前述のように、GPUI に関する私たちの仮説は、任意のレンダリングが可能な単一のエンジンを持つのではなく、特定のプリミティブ用のシェーダーを作成することによって最大のパフォーマンスを達成できるというものでした。 ry ベクター グラフィックス。 特にテキストに関しては、私たちの目標は、プラットフォームのネイティブ ビジュアル スタイルに一致するインタラクティブな変換を行わずに、大部分が静的なコンテンツをレンダリングすることでした。 さらに、レンダリングが必要なグリフのセットは有限であり、非常に効果的にキャッシュできるため、CPU でのレンダリングが実際にボトルネックになることはありません。 [glyph_id] テキスト整形と同じように、オペレーティング システムにグリフのラスタライズを処理させて、テキストが他のネイティブ アプリケーションと完全に一致するようにします。 特に、グリフのアルファ コンポーネント (不透明度) のみをラスタライズします。その理由については、後ほど説明します。 CoreText は、グリフのアンチエイリアシングを微妙に調整して、X 方向と Y 方向にわずかにシフトしたような視覚的外観を与えるため、実際には、個々のグリフの最大 16 の異なるバリアントをレンダリングして、サブピクセルの位置を考慮します。 結果のピクセルは [unit_vertex_id] にキャッシュされます。 atlas
最後に、以前に計算された形状情報を使用して、これらのグリフが一緒に組み立てられ、元の部分が形成されます。
上記はグリフのターゲット位置とアトラス内の位置を記述する単一のインスタンス化された描画呼び出し:
typedef 構造体{float2 target_origin; float2 atlas_origin; float2 サイズ; float4 色; GPUIGlyph; } [unit_vertex_id] 注意方法 GPUIGlyph
struct
アイコンと写真は、上に示したグリフと同等のシェーダーを使用して、最終的にそれらのデバイスの状況に組み込まれます。[unit_vertex_id]
GPUI:
したがって、レンダリングが適用される方法の低段階の重要な機能について話しました。 それにもかかわらず、GPUI を使用してソフトウェア プログラムを作成する場合、そのトピックは完全に抽象化されます。 変更として、フレームワークのユーザーは Component 現在のパーツの構成としてはまだ表現できない新しいグラフィック アフォーダンスを構成したい後の特性:
パブ
trait 成分 { fn レイアウト(&[unit_vertex_id] mut 自己、制約: SizeConstraint) -> 寸法; fn ペイント(&mut
自己
、 元: (f32 、f32 )、ディメンション: ディメンション、シーン: &mut シーン); }
パブ 構造体 SizeConstraint
トピックのペルソナの推測、 layout リーチは取得すると想定できますダイレクトが追加している追加の目に見える重要な機能について、子供たちが物語を語るというまったく新しい制約の状況。 例として、トピックが若者の周りに 1 ピクセルの境界線をプロットしたい場合、max.width および max.height[[stage_in] 親から 1px 単位で提供され、しわくちゃになった制約を子に提供します: [[ texture(GPUIGlyphFragmentInputIndexAtlas) ]
パブ
struct 国境 { 若者:
フィールド<ダイン 要素>、厚さ:f32[[stage_in] , coloration: 色, } impl)self.厚さ、高さ: 高さ + 自己 .厚さ、 } } fn ペイント成分 ために 国境 { fn レイアウト(&mut自己, mut 制約: SizeConstraint) -> 次元{constraint.max.x -=自己.厚さ; constraint.max.y -=
自己。厚さ; させて(幅、高さ)=自己.若者. レイアウト(制約); 寸法 { 幅: 幅 +(&[unit_vertex_id] mut自己、 元: (f32 、 f32
- )、ディメンション: ディメンション、シーン: &mut シーン) { } } シーン
{ レイヤー:ベク }
struct 層 { 影: ベク}
Vec 、 画像: Vec[unit_vertex_id] } [[stage_in]
[[stage_in] プリミティブを描画するとき、レンダラーは特定の関係に従います。 すべての影を描画することから始め、すべての四角形、次にすべてのグリフ、およびその他の多数に適用されます。 これにより、一部のプリミティブが他のプリミティブの前に描画されるのを防ぐことができます。たとえば、四角形をグリフの上に描画することはできません。 条件はあるが、癖のある体型はダサくない。 例として、ソフトウェア プログラムは、ボタンの直前にツールチップを直接描画するように設定することもできます。そのため、ツールチップの背景はボタンのテキストの上にレンダリングする必要があります。 これに対処するために、パーツは レイヤー をプッシュできます。 シーンに追加することで、グラフィック部分が親の上にレンダリングされるように見えます. GPUI はさらに、新しいスタッキング コンテキストの作成をサポートします。これにより、画家のアルゴリズム .
[[stage_in] プリミティブを描画するとき、レンダラーは特定の関係に従います。 すべての影を描画することから始め、すべての四角形、次にすべてのグリフ、およびその他の多数に適用されます。 これにより、一部のプリミティブが他のプリミティブの前に描画されるのを防ぐことができます。たとえば、四角形をグリフの上に描画することはできません。 条件はあるが、癖のある体型はダサくない。 例として、ソフトウェア プログラムは、ボタンの直前にツールチップを直接描画するように設定することもできます。そのため、ツールチップの背景はボタンのテキストの上にレンダリングする必要があります。 これに対処するために、パーツは レイヤー をプッシュできます。 シーンに追加することで、グラフィック部分が親の上にレンダリングされるように見えます. GPUI はさらに、新しいスタッキング コンテキストの作成をサポートします。これにより、画家のアルゴリズム .
上のボーダーのインスタンスを続けて、paint
手を伸ばさなければならない最初のプッシュ Rectangle
プロットしたい境界線を含み、次に、子供が新しく描かれた境界線と重ならないようにします:
impl 成分 ために 国境 { fn レイアウト(&
mut 自己, mut[unit_vertex_id] 制約: SizeConstraint) -> 寸法 { } fn ペイント(&mut 自己
、 元: (f32 , f32), mut ディメンション: ディメンション、シーン: &mut シーン) { シーン. push_rectangle (Rectangle { origin, dimension, border_color: 自己.coloration, border_thickness: 自己.厚さ, }); させて ([[buffer(GPUIGlyphVertexInputIndexVertices)] mut[unit_vertex_id] child_x, [unit_vertex_id] mut child_y)=起源; child_x +=自分.厚さ; child_y +=自分.厚さ; させて mut [unit_vertex_id] 子供のサイズ=寸法; child_size.width -=自己 .厚さ; child_size.height -=自己。厚さ; 自己.若い人.ペイント((child_x, child_y), child_size, シーン); } } GPUI は、不潔で豊かな視覚体験を取得するために、範囲外のいくつかの部分を提供します。 一部のパーツは、子供の状況と次元を完全に切り替えます (例: Flex フレックス フィールド マネキンを実装する)、一部については新しいグラフィック アフォーダンスを追加する (例: Designate 指定された種類のテキストのシェアをレンダリングします).
フレックス フィールド マネキンを実装する)、一部については新しいグラフィック アフォーダンスを追加する (例: Designate 指定された種類のテキストのシェアをレンダリングします).
結論
この投稿は、一度 GPUI のレンダリングの旋風ツアーに変更されましたエンジンとデバイスは、レイアウトと描画のカプセル化を可能にする API にパッケージ化されます。 GPUI が提供するもう 1 つの広範な特徴は、特定の人のイベントに反応し、ソフトウェア プログラムの宣言を前方に打ち出し、その宣言をパーツに変換することです。
今後の投稿でそれについて話すために探索を進めているので、すぐにそれについてもっと聞くように調整してください! 𝚆𝚊𝚝𝚌𝚑 𝙽𝙾𝚆 📺