Newer
Older
SmTIAS-LightSim / src / sm_tias_lightsim / render / pipeline.py
"""レンダパイプラインの本体(sm-render の処理を関数として提供する).

`render_yaml()` が「シーン YAML 読込 → variant 解決 → レンダ → render.png /
luminance.npy 保存」を行う.CLI(`cli/render.py`)は引数のパースと,
例外 → 「エラー: …」表示 + 終了コード 1 への変換だけを担う.

スイープスクリプトのように何十条件も回す用途では,サブプロセス(`uv run sm-render`)の
起動と `import mitsuba` が 1 条件あたり 2 秒前後かかるため,本関数をプロセス内で
繰り返し呼ぶことで丸ごと省ける.

mitsuba に触れる import(runner / builder / schema)はすべて関数内で行う.
モジュールレベルで import すると,llvm_* variant 用の DRJIT_LIBLLVM_PATH 設定より前に
LLVM 初期化が走ってしまうため(cli/render.py と同じ遅延 import 方針).
"""

from __future__ import annotations

from pathlib import Path

# Step 6 の evaluate CLI も同じファイルを読むため,名前を定数として一元管理する
_PNG_FILENAME = "render.png"
_NPY_FILENAME = "luminance.npy"


class RenderStepError(RuntimeError):
    """レンダパイプラインのどの段階で失敗したかを文言として保持する例外.

    CLI は `print(f"エラー: {exc}")` するだけで従来どおりの段階別メッセージを出せる.
    error-handling.md の「独自例外は最小限」に沿って階層は作らず,この 1 クラスのみとする.
    """


def render_yaml(
    scene_yaml: Path,
    output_dir: Path,
    variant: str | None = None,
) -> tuple[Path, Path]:
    """シーン YAML をレンダリングし,render.png と luminance.npy を出力する.

    処理フロー:
        1. YAML シーン定義を load_scene_yaml で読み込み,SceneConfig を得る.
        2. variant を解決する(引数 > 環境変数 SM_RENDER_VARIANT > YAML の render.variant).
        3. 出力ディレクトリを作成する(親ごと,既存なら何もしない).
        4. llvm_* のとき DRJIT_LIBLLVM_PATH を設定してから render_scene を実行する.
        5. render.png と luminance.npy を保存する.

    Args:
        scene_yaml: シーン定義 YAML ファイルのパス.
        output_dir: 出力先ディレクトリ(存在しない場合は親ごと作成する).
        variant: Mitsuba variant の明示指定.None なら環境変数 / YAML から解決する.

    Returns:
        (render.png のパス, luminance.npy のパス).

    Raises:
        RenderStepError: いずれかの段階で失敗した場合(失敗した段階を示す文言を持つ).
    """
    scene_path = Path(scene_yaml).resolve()
    out_dir = Path(output_dir).resolve()

    try:
        from sm_tias_lightsim.scene.schema import load_scene_yaml

        config = load_scene_yaml(scene_path)
    except FileNotFoundError as exc:
        msg = f"シーン YAML ファイルが見つかりません — {exc}"
        raise RenderStepError(msg) from exc
    except Exception as exc:
        msg = f"シーン YAML の読み込みに失敗しました — {exc}"
        raise RenderStepError(msg) from exc

    # 同じ YAML を手元(llvm)とサーバ(cuda)で使い回すため,variant だけ実行側から差し替える
    # 優先順位: 引数 variant > 環境変数 SM_RENDER_VARIANT > YAML の render.variant
    from sm_tias_lightsim.runtime.overrides import resolve_render_variant

    resolved = resolve_render_variant(config.render.variant, variant)
    if resolved != config.render.variant:
        print(f"variant を上書き: {config.render.variant} → {resolved}")
        config = config.model_copy(
            update={"render": config.render.model_copy(update={"variant": resolved})}
        )

    try:
        out_dir.mkdir(parents=True, exist_ok=True)
    except OSError as exc:
        msg = f"出力ディレクトリを作成できません — {exc}"
        raise RenderStepError(msg) from exc

    # project_root を scene YAML の親ディレクトリとすることで,
    # YAML 内の STL 相対パスが YAML ファイルを基点に解決される
    project_root = scene_path.parent
    try:
        # llvm_* variant の場合,mitsuba の import チェーン(sensors.py / lights.py)が
        # 走る前に DRJIT_LIBLLVM_PATH を設定しておく必要がある.
        # runner.py の import(build_scene → sensors.py / lights.py → mitsuba)より先に呼ぶ.
        if config.render.variant.startswith("llvm_"):
            from sm_tias_lightsim.runtime.llvm_init import ensure_drjit_libllvm_path

            ensure_drjit_libllvm_path()
        from sm_tias_lightsim.render.runner import render_scene

        hdr_image = render_scene(config, project_root=project_root)
    except Exception as exc:
        msg = f"レンダリングに失敗しました — {exc}"
        raise RenderStepError(msg) from exc

    png_path = out_dir / _PNG_FILENAME
    npy_path = out_dir / _NPY_FILENAME
    try:
        from sm_tias_lightsim.render.output import save_luminance, save_png

        save_png(hdr_image, png_path)
        save_luminance(hdr_image, npy_path)
    except Exception as exc:
        msg = f"出力ファイルの保存に失敗しました — {exc}"
        raise RenderStepError(msg) from exc

    return png_path, npy_path