Skip to content

Architecture

github-actions[bot] edited this page Aug 20, 2026 · 7 revisions

Home

Architecture

FloatSoda は Flutter のアーキテクチャを参考に設計された VR オーバーレイ UI フレームワークです。SkiaSharp で描画コマンドを記録し、OpenGL テクスチャに焼き付けて OpenVR Compositor に提出することで SteamVR オーバーレイを表示します。

アセンブリ構成

graph TD
    Abstractions["FloatSoda.Abstractions\n共有契約・プリミティブ"]
    Rendering["FloatSoda.Rendering\nLayerツリー・Skia描画"]
    Engine["FloatSoda.Engine\nOpenGL・レンダースレッド"]
    OVR["FloatSoda.OVR\nOpenVR ラッパー"]
    Core["FloatSoda\nウィジェット・RenderObject・パイプライン"]
    Testing["FloatSoda.Testing\nヘッドレス画像レンダリング"]
    UI["FloatSoda.UI\nヘッドレスUI(振る舞いのみ)"]
    Cream["FloatSoda.UI.Cream\nデザインシステム①"]
    FizzyPop["FloatSoda.UI.FizzyPop\nデザインシステム②"]

    Rendering --> Abstractions
    Rendering --> Engine
    Rendering --> Core
    Abstractions --> Engine
    Abstractions --> Core
    OVR --> Core
    Engine --> Core
    Core --> Testing
    Rendering --> Testing
    Core --> UI
    UI --> Cream
    UI --> FizzyPop
Loading
アセンブリ 役割
FloatSoda.Abstractions Engine境界契約、Offsetなどの共有値型、入力イベント、フレームペーシング
FloatSoda.Rendering ILayerと具象Layer群、共通Layer描画、Bitmap描画
FloatSoda.Engine IEngineWindowなどの具体実装、GLViewRendererRenderPostTaskRunnerIOTaskRunnerFramePacer
FloatSoda.OVR OpenVR 初期化(Application)、オーバーレイ型(DashboardOverlay / WorldSpaceOverlay / DeviceTrackedOverlay)、イベントディスパッチャ、例外体系
FloatSoda ウィジェット/エレメントツリー、RenderObject ツリー、RenderPipelineFloatSodaApp、Generic Host統合
FloatSoda.Testing Widget・RenderObjectツリーをBitmapへ描画するヘッドレステスト支援
FloatSoda.UI ヘッドレスUI層。振る舞い・状態機械のみ(ButtonBase, InteractionState)。見た目は builder に委譲。Phase 5 の予定で未提供(→ UILayering)
FloatSoda.UI.Cream デザインシステム①: レトロでクリーミーな色使いのフラットデザイン(Button, ButtonStyle, CreamTheme)。Phase 5 の予定で未提供
FloatSoda.UI.FizzyPop デザインシステム②: 透明感・グラスモーフィズム(Button, ButtonStyle, FizzyPopTheme)。Phase 5 の予定で未提供
FloatSoda.Hooks R3 ベースの HookWidget / HookElement。フレームワークのビルドループとは未統合(部分実装)

ツリー構造

FloatSoda は Flutter の三ツリーモデルをベースに、現在 RenderObject ツリーレイヤーツリー が完全実装済みです。ウィジェット/エレメントツリーは StatelessWidget / StatefulWidget / InheritedWidget / ParentDataWidget<T> と、BuildOwner による差分ビルド(Key 対応の子リスト差分を含む)が実装済みです。レイアウト・描画・入力系のウィジェットは一巡し、残る未実装はスクロール系(ListView / GridView / SingleChildScrollView)です(詳細は WidgetSystemBuildPipeline)。

graph LR
    subgraph "Widget / Element Tree"
        W["Widget\n(immutable record)"]
        E["Element\n(mutable)"]
        BO["BuildOwner\n(dirty list)"]
        W -->|CreateElement| E
        BO -->|"BuildScope() で\ndirty Element を再ビルド"| E
    end

    subgraph "RenderObject Tree"
        RV["RenderView"]
        RB["RenderBox\nサブクラス群"]
        RV --> RB
    end

    subgraph "Layer Tree"
        CL["ContainerLayer"]
        PL["PictureLayer\n(SKPicture)"]
        CL --> PL
    end

    E -->|"CreateRenderObject /\nUpdateRenderObject"| RV
    RB -->|"Paint → PaintingContext"| CL
Loading

各ツリーの役割:

  • Widget — 宣言的な UI の設計図。abstract record で不変。
  • Element — Widget と RenderObject を橋渡しするミュータブルなノード。MarkNeedsBuild() で dirty になり、BuildOwner が次フレームの BuildScope() でまとめて再ビルドする。StatelessElement / StatefulElement / InheritedElement はいずれも実装済み。
  • BuildOwner — dirty な Element のリストを保持し、Depth 順(親が先)に再ビルドを実行するスケジューラ。WidgetBinding がウィンドウごとに 1 つ保持する。
  • RenderObject — レイアウト計算(PerformLayout)と描画コマンド記録(Paint)を担う。MarkNeedsLayout / MarkNeedsPaint の dirty フラグにより、変更があった部分だけを再レイアウト・再ペイントする。
  • LayerPaint フェーズが生成する合成操作のツリー。クローンしてレンダースレッドに渡す。

レンダリングライフサイクル

sequenceDiagram
    participant Host as Generic Host
    participant Main as FloatSodaメインスレッド (STA)
    participant WB as WidgetBinding
    participant BO as BuildOwner
    participant Pipeline as RenderPipeline
    participant RV as RenderView
    participant RO as RenderObject 群
    participant PC as PaintingContext
    participant Layer as レイヤーツリー
    participant RT as レンダースレッド
    participant Renderer as Renderer
    participant GL as GLView (GLFW/OpenGL)
    participant VR as OpenVR Compositor

    Host->>Main: FloatSodaHostedService.StartAsync()

    loop メインループ (FloatSodaApp.MainLoop)
        Main->>Main: VREventDispatcher.PollEvents()
        Note over Main,Host: VREvent_Quit → Host全体へ停止通知

        Main->>WB: DrawFrame() [ウィンドウごと]
        WB->>BO: BuildScope()
        Note over BO: dirty な Element を Depth 順に Rebuild<br/>→ UpdateRenderObject でプロパティ反映

        alt NeedsVisualUpdate == true
            WB->>Pipeline: FlushLayout()
            Pipeline->>RO: LayoutWithoutResize() [dirty ノードのみ・Depth 順]
            Note over RO: 制約を子へ伝播し<br/>SKSize を親に返す

            WB->>Pipeline: FlushPaint()
            Pipeline->>PC: RepaintCompositedChild(node) [dirty ノードのみ]
            RV->>RO: Paint(context, offset) [再帰]
            Note over RO: context.Canvas に Skia ドローコール記録<br/>クリップ等は PushLayer で子 ContainerLayer に分岐
            PC->>Layer: PictureLayer を ContainerLayer に追加
            Note over PC: SKPictureRecorder.EndRecording()<br/>→ PictureLayer.Picture に保存

            WB->>Layer: Layer.Clone() — スレッドセーフコピー
            WB-->>RT: PostRender(window, capturedLayer)
        end

        Main->>Main: FramePacer.WaitForNextFrame()
    end

    loop レンダースレッド (RenderPostTaskRunner)
        RT->>RT: ConcurrentQueue からタスクを取り出す
        RT->>Renderer: Render(layer)
        Renderer->>GL: Clear()
        Note over GL: GRContext.ResetContext()<br/>SKSurface.Canvas.Clear(Transparent)
        Renderer->>Layer: layer.Layout(LayerContext)
        Note over Layer: PaintBounds を再帰的に計算
        Renderer->>Layer: layer.Paint(LayerContext)
        Note over Layer: ContainerLayer → PictureLayer の順に<br/>SKCanvas.DrawPicture() で描画<br/>ClipLayer は SaveLayer/ClipXxx を挿入
        Renderer->>GL: Flush()
        Note over GL: SKSurface.Flush() → GRContext.Flush()<br/>→ GL.Flush() で GL テクスチャに書き込み完了
        RT->>VR: Overlay.Texture.FromTexture_t(GL texture handle)
        Note over VR: ETextureType.OpenGL / EColorSpace.Auto
    end
Loading

スレッドモデル

スレッド 所有物 通信方法
メインスレッド RenderPipeline, Widget/RenderObject ツリー, VREventDispatcher RenderPostTaskRunner.PostTask(Action) でタスクをキューに積む
レンダースレッド OpenGL コンテキスト, GLView, Renderer, OverlayWindow ConcurrentQueue<Action> を処理
I/Oスレッド ImageProvider / FontProvider によるリソース読み込み IOTaskRunner.RunAsync(...) に投入し、完了は BuildOwnerTaskScheduler 経由でメインスレッドへ戻る

OpenGL のコンテキストはレンダースレッドが独占します。ウィンドウ作成も PostTask 経由でレンダースレッド上で実行されます。

I/O スレッドは IOTaskRunner が持つ単一のバックグラウンドスレッドで、投入順に1件ずつ処理します。Image の画像読み込みはこのスレッドで行われ、完了後の再ビルドはメインスレッドの BuildScope で実行されます。なお、テキストレイアウト中のフォント解決は同期 API から呼ばれるため、このキューを経由せず呼び出しスレッド上で読み込みます。

メインスレッド
  └─ RenderPostTaskRunner.PostTask(layer 更新ラムダ)
         │ ConcurrentQueue
         ▼
    レンダースレッド
         └─ OverlayWindow.Update()
              └─ Renderer.Render(layer)
                   └─ GLView.Clear() → layer.Paint() → GLView.Flush()
              └─ SetOverlayTexture(GL texture handle)

関連ページ

  • BuildPipeline — BuildOwner による差分ビルドの詳細
  • RenderObjects — 差分レイアウト・差分ペイントの仕組み
  • OVRIntegration — OpenVR オーバーレイとイベント処理

Clone this wiki locally