linear-gradient のラスタライズ最適化

背景(実測)

描画 1 フレームの内訳:

[PERF][DRAW] head=1tick imgprep=0tick paint=304tick bgimg=35tick grad=112tick border=0tick
[PERF][DRAW] head=1tick imgprep=0tick paint=302tick bgimg=33tick grad=116tick border=0tick

グラデーションが単独で最大の要素。

現状の実装

draw_linear_gradient は 1 ピクセルごとに:

  1. 射影 p = x·dx + y·dy を計算
  2. t = (p - min_proj) / length
  3. sample_gradient_stops(stops, t) — ストップ配列を線形走査し、 4 チャンネルを浮動小数で補間
  4. draw_pixel_alpha

ヒーロー領域が 924×600 なら 55 万回これを回す。

最適化

O-1: 色の LUT 化

t から色への写像はピクセル位置に依存しない。 グラデーション軸方向の長さ length ぶんの LUT を先に作れば、 sample_gradient_stops(ストップ走査+浮動小数補間)の呼び出しは W×H 回から length 回へ落ちる。

LUT の分解能は ceil(length) 段とする。これは グラデーション軸方向の 1 ピクセルに 1 段を割り当てる意味で、

O-2: 軸平行の行/列一括塗り

dx == 0(0deg / 180deg)なら t は y だけの関数。 1 行が単色になるので、行ごとに boxfill 1 回で済む。 dy == 0(90deg / 270deg)なら列方向に同じことが言える (ただしフレームバッファは行優先なので、行内を走査して 同色の連続区間をまとめる形にする)。

これで 55 万回のピクセル演算が数百回の矩形塗りになる。

不変条件

モジュール

web_engine/gradient_lut.rs(純粋・no_std・グローバル状態なし)

/// LUT の段数を決める。length は射影長(ピクセル)。
pub fn lut_size(length: f32) -> usize;

/// t(0.0-1.0 想定、範囲外はクランプ)を LUT の添字へ。
pub fn lut_index(t: f32, size: usize) -> usize;

/// dx, dy から軸平行かどうかを判定する。
pub enum Axis { Vertical, Horizontal, Diagonal }
pub fn classify_axis(dx: f32, dy: f32) -> Axis;

実装後の実測

              grad
修正前        112 tick
修正後         62〜70 tick

約 43% 短縮(make test 744 件パス、test_gradient_lut 24 件を新規追加)。

効いたのは LUT。行一括塗りはほぼ発火していない

実測の下がり幅から見て、効果はほぼ O-1(LUT 化)によるもの。 このサイトのグラデーションは 135deg 等の斜めが主で、 O-2(軸平行の行一括塗り)の経路にはほとんど入っていない。

O-2 は縦(0/180deg)グラデーションのあるページでは大きく効くはずだが、 このページでは確認できていない。効いたと書かないこと。

残り

paint 約 300 tick のうち: - グラデーション 65 tick(22%) - 背景画像 38 tick(13%) - 残り約 200 tick(65%)は未特定

次はテキスト描画と矩形塗りを分けて測る。