Javaにおけるダブルバッファパターン:アニメーションとグラフィックスのパフォーマンス向上
別名
- バッファ切り替え
- ピンポンバッファ
ダブルバッファデザインパターンの意図
Javaにおけるダブルバッファパターンは、2つのバッファを利用することで、グラフィカルまたは計算アプリケーションのレンダリング時間を短縮し、パフォーマンスを向上させるように設計されています。このパターンは、スムーズなグラフィックスレンダリングに不可欠であり、ゲーム開発やその他のリアルタイムアプリケーションで一般的に使用されています。
実世界の例を用いたダブルバッファパターンの詳細な説明
実世界の例
常に料理を準備しているシェフと、常に出来上がった料理をピックアップして顧客にサービスを提供しているウェイターが忙しいレストランのキッチンを想像してみてください。混乱や遅延を避けるため、レストランではダブルバッファシステムを使用しています。彼らは2つのカウンターを持っています:1つはシェフが新しく準備した料理を置くためのもので、もう1つはウェイターが料理をピックアップするためのものです。シェフが1つのカウンターを準備された料理で満たしている間、ウェイターは同時に別のカウンターをサービスのために料理をピックアップして片付けています。ウェイターがカウンターからすべての料理を片付けると、シェフが新しく準備した料理を置いたカウンターに切り替え、シェフは空になったカウンターを満たし始めます。このシステムは、どちらの側も無駄に待つことなく、スムーズで継続的なワークフローを保証し、効率を最大化し、ダウンタイムを最小限に抑えます。
平易な言葉で言うと
状態が段階的に変更されている間、その状態が正しくレンダリングされることを保証します。コンピュータグラフィックスで広く使用されています。
Wikipediaによると
コンピュータサイエンスでは、マルチバッファリングは、データのブロックを保持するために複数のバッファを使用することです。これにより、「リーダー」は、「ライター」によって作成されているデータの部分的に更新されたバージョンではなく、データの完全な(ただし、古い可能性がある)バージョンを見ることができます。これは、コンピュータディスプレイ画像で非常に一般的に使用されています。
Javaでのダブルバッファパターンのプログラム例
一般的な例であり、すべてのゲームエンジンが対処する必要があるのは、レンダリングです。ゲームがユーザーが見る世界を描画するとき、それは一度に1つのピースずつ行います - 遠くの山々、なだらかな丘、木々など、それぞれが順番に行われます。ユーザーがそのように増分的にビューを描画するのを見た場合、一貫した世界の錯覚は打ち砕かれるでしょう。シーンはスムーズかつ迅速に更新し、一連の完全なフレームを瞬時に表示する必要があります。ダブルバッファリングは問題を解決します。
バッファの基本的な機能を保証するBuffer
インターフェース。
public interface Buffer {
void clear(int x, int y);
void draw(int x, int y);
void clearAll();
Pixel[] getPixels();
}
Buffer
インターフェースの実装の1つ。
public class FrameBuffer implements Buffer {
public static final int WIDTH = 10;
public static final int HEIGHT = 8;
private final Pixel[] pixels = new Pixel[WIDTH * HEIGHT];
public FrameBuffer() {
clearAll();
}
@Override
public void clear(int x, int y) {
pixels[getIndex(x, y)] = Pixel.WHITE;
}
@Override
public void draw(int x, int y) {
pixels[getIndex(x, y)] = Pixel.BLACK;
}
@Override
public void clearAll() {
Arrays.fill(pixels, Pixel.WHITE);
}
@Override
public Pixel[] getPixels() {
return pixels;
}
private int getIndex(int x, int y) {
return x + WIDTH * y;
}
}
白黒ピクセルをサポートしています。
public enum Pixel {
WHITE,
BLACK
}
Scene
は、現在のバッファがすでにレンダリングされているゲームシーンを表します。
@Slf4j
public class Scene {
private final Buffer[] frameBuffers;
private int current;
private int next;
public Scene() {
frameBuffers = new FrameBuffer[2];
frameBuffers[0] = new FrameBuffer();
frameBuffers[1] = new FrameBuffer();
current = 0;
next = 1;
}
public void draw(List<? extends Pair<Integer, Integer>> coordinateList) {
LOGGER.info("Start drawing next frame");
LOGGER.info("Current buffer: " + current + " Next buffer: " + next);
frameBuffers[next].clearAll();
coordinateList.forEach(coordinate -> {
var x = coordinate.getKey();
var y = coordinate.getValue();
frameBuffers[next].draw(x, y);
});
LOGGER.info("Swap current and next buffer");
swap();
LOGGER.info("Finish swapping");
LOGGER.info("Current buffer: " + current + " Next buffer: " + next);
}
public Buffer getBuffer() {
LOGGER.info("Get current buffer: " + current);
return frameBuffers[current];
}
private void swap() {
current = current ^ next;
next = current ^ next;
current = current ^ next;
}
}
次に、ダブルバッファリングの例を駆動するApp
クラスを示すことができます。
@Slf4j
public class App {
public static void main(String[] args) {
final var scene = new Scene();
var drawPixels1 = List.of(
new MutablePair<>(1, 1),
new MutablePair<>(5, 6),
new MutablePair<>(3, 2)
);
scene.draw(drawPixels1);
var buffer1 = scene.getBuffer();
printBlackPixelCoordinate(buffer1);
var drawPixels2 = List.of(
new MutablePair<>(3, 7),
new MutablePair<>(6, 1)
);
scene.draw(drawPixels2);
var buffer2 = scene.getBuffer();
printBlackPixelCoordinate(buffer2);
}
private static void printBlackPixelCoordinate(Buffer buffer) {
StringBuilder log = new StringBuilder("Black Pixels: ");
var pixels = buffer.getPixels();
for (var i = 0; i < pixels.length; ++i) {
if (pixels[i] == Pixel.BLACK) {
var y = i / FrameBuffer.WIDTH;
var x = i % FrameBuffer.WIDTH;
log.append(" (").append(x).append(", ").append(y).append(")");
}
}
LOGGER.info(log.toString());
}
}
コンソール出力
12:33:02.525 [main] INFO com.iluwatar.doublebuffer.Scene -- Start drawing next frame
12:33:02.529 [main] INFO com.iluwatar.doublebuffer.Scene -- Current buffer: 0 Next buffer: 1
12:33:02.529 [main] INFO com.iluwatar.doublebuffer.Scene -- Swap current and next buffer
12:33:02.529 [main] INFO com.iluwatar.doublebuffer.Scene -- Finish swapping
12:33:02.529 [main] INFO com.iluwatar.doublebuffer.Scene -- Current buffer: 1 Next buffer: 0
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Get current buffer: 1
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.App -- Black Pixels: (1, 1) (3, 2) (5, 6)
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Start drawing next frame
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Current buffer: 1 Next buffer: 0
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Swap current and next buffer
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Finish swapping
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Current buffer: 0 Next buffer: 1
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.Scene -- Get current buffer: 0
12:33:02.530 [main] INFO com.iluwatar.doublebuffer.App -- Black Pixels: (6, 1) (3, 7)
Javaでダブルバッファパターンを使用する状況
- リアルタイムアプリケーション:頻繁かつスムーズな表示更新が不可欠なビデオゲーム、シミュレーション、GUIアプリケーションに最適です。
- 高計算タスク:集中的なデータ準備を必要とするアプリケーションに適しており、並列処理と表示を可能にします。
- ラグの最小化:データまたはグラフィック表示のラグやスタッターを軽減するのに効果的です。
Javaでのダブルバッファパターンの実世界での応用
- グラフィックスレンダリングエンジン:滑らかなアニメーションとトランジションを保証するために、2Dおよび3Dレンダリングエンジンで広く使用されています。
- GUIフレームワーク:ユーザーインターフェイスの応答性と滑らかさを向上させます。
- シミュレーションとモデリング:進行中のプロセスを中断することなく、シミュレーションでのリアルタイム更新を保証します。
- ビデオ再生ソフトウェア:現在のフレームの表示中に次のフレームをプリロードすることにより、シームレスなビデオ再生を提供します。
ダブルバッファパターンのメリットとトレードオフ
メリット
- スムーズなユーザーエクスペリエンス:スムーズなアニメーションとトランジションを提供するためにフレームをプリレンダリングします。
- パフォーマンス最適化:バックグラウンドレンダリングを可能にし、アプリケーション全体のパフォーマンスを最適化します。
- ちらつきの最小化:グラフィックアプリケーションでのちらつきや視覚的なアーティファクトを減らします。
トレードオフ
- メモリオーバーヘッド:セカンダリバッファに追加のメモリが必要となり、メモリ使用量が増加する可能性があります。
- 実装の複雑さ:アーキテクチャに複雑さを加え、慎重なバッファ管理が必要になります。
- レイテンシ:データが表示前にバックバッファで完全にレンダリングされる必要があるため、わずかな遅延が発生する可能性があります。
関連するJavaデザインパターン
- トリプルバッファリング:ダブルバッファパターンの拡張であり、3つのバッファを使用してレンダリングをさらに最適化し、レイテンシを削減します。
- プロデューサーコンシューマー:ダブルバッファパターンは、一方のバッファが「生成」され、もう一方のバッファが「消費」される、プロデューサーコンシューマーパターンのバリエーションと見なすことができます。
- ストラテジー:実行時条件に基づいてバッファリング戦略を動的に選択するために、ストラテジーパターンと組み合わせて使用されることがよくあります。