diff --git a/apps/crazy_robotaxi/README.md b/apps/crazy_robotaxi/README.md index 9aaa85333..886995550 100644 --- a/apps/crazy_robotaxi/README.md +++ b/apps/crazy_robotaxi/README.md @@ -335,7 +335,7 @@ in the live-edit HUD card alongside frame-aligned ability status. Weather cannot change while a non-base style is active. Required additional checkpoints are downloaded during application startup when the features are enabled, unless explicit checkpoint paths are configured. Downloaded assets are cached under -`artifacts/crazy_robotaxi/live_edit` relative to the working directory. +`$FLASHDREAMS_CACHE_DIR/crazy-robotaxi/live_edit`. Dynamic prompts append selected fragments for the current road context, next map-node type, next map-node context, upcoming road-curve direction, and taxi diff --git a/apps/crazy_robotaxi/crazy_robotaxi/live_edit/config.py b/apps/crazy_robotaxi/crazy_robotaxi/live_edit/config.py index 16d3544dc..2a4ed1cba 100644 --- a/apps/crazy_robotaxi/crazy_robotaxi/live_edit/config.py +++ b/apps/crazy_robotaxi/crazy_robotaxi/live_edit/config.py @@ -18,10 +18,10 @@ from omnidreams_game_engine.cli_args import arg_was_explicit +from flashdreams.core.io.disk import default_flashdreams_cache_dir from flashdreams.core.io.download import download_to_cache _CORRECTOR_MODES = ("fused", "unfused", "off") -_DEFAULT_ASSET_DIR = Path("artifacts/crazy_robotaxi/live_edit") _STYLE_LORA_URL = ( "https://github.com/wenqingw-nv/flashdreams-wq/releases/download/" "style-skin-v6-multiskin/lora_style_v6_step1600.pt" @@ -799,7 +799,7 @@ def __post_init__(self) -> None: def resolve_live_edit_assets( config: LiveEditConfig, *, - cache_dir: Path = _DEFAULT_ASSET_DIR, + cache_dir: Path | None = None, ) -> LiveEditConfig: """Download missing checkpoints needed by enabled live-edit features. @@ -809,6 +809,8 @@ def resolve_live_edit_assets( OmniDreams clean-forcing checkpoint. Weather only needs a corrector when its configured gain is nonzero; otherwise weather remains LoRA-free. """ + if cache_dir is None: + cache_dir = default_flashdreams_cache_dir() / "crazy-robotaxi" / "live_edit" style = config.style weather = config.weather if style.enabled and style.lora_checkpoint is None: diff --git a/apps/crazy_robotaxi/crazy_robotaxi/ui.py b/apps/crazy_robotaxi/crazy_robotaxi/ui.py index f53b3bd64..89a3ec205 100644 --- a/apps/crazy_robotaxi/crazy_robotaxi/ui.py +++ b/apps/crazy_robotaxi/crazy_robotaxi/ui.py @@ -3897,6 +3897,15 @@ def step_ui( for event in events.get_events(): if isinstance(event, FullscreenEnabledOptimalWindowSizeUserInputEvent): self.state.presentation_size = event.width, event.height + if (document := self.state.settings_document) is not None: + document.settings = replace( + document.settings, + presentation=replace( + document.settings.presentation, + width=event.width, + height=event.height, + ), + ) presentation_size = self.state.presentation_size or ( self.state.width, self.state.height, diff --git a/apps/crazy_robotaxi/tests/test_live_edit_v2.py b/apps/crazy_robotaxi/tests/test_live_edit_v2.py index 6f5699da0..bbf90d53d 100644 --- a/apps/crazy_robotaxi/tests/test_live_edit_v2.py +++ b/apps/crazy_robotaxi/tests/test_live_edit_v2.py @@ -144,6 +144,21 @@ def fake_download(url: str, *, cache_dir: Path) -> Path: assert len(downloads) == 4 +def test_style_assets_use_flashdreams_cache_by_default( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("FLASHDREAMS_CACHE_DIR", str(tmp_path)) + cache_dirs: list[Path] = [] + + def fake_download(url: str, *, cache_dir: Path) -> Path: + cache_dirs.append(cache_dir) + return cache_dir / Path(url).name + + monkeypatch.setattr(live_edit_config, "download_to_cache", fake_download) + resolve_live_edit_assets(LiveEditConfig(style=LiveEditStyleConfig(enabled=True))) + assert cache_dirs == [tmp_path / "crazy-robotaxi" / "live_edit"] * 4 + + def test_explicit_style_assets_skip_download( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/apps/crazy_robotaxi/tests/test_ui.py b/apps/crazy_robotaxi/tests/test_ui.py index 8a0436b60..4763c0d7f 100644 --- a/apps/crazy_robotaxi/tests/test_ui.py +++ b/apps/crazy_robotaxi/tests/test_ui.py @@ -73,9 +73,13 @@ def test_taxi_ui_requests_fullscreen_dimensions_before_resizing( monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, ) -> None: loop = CrazyRobotaxiImGuiUILoop(width=320, height=240) - loop.state = TaxiHudState(320, 240, None, presentation_size=(320, 240)) + document = _settings_document(tmp_path / "config.yaml") + loop.state = TaxiHudState( + 320, 240, None, presentation_size=(320, 240), settings_document=document + ) loop._ui_loop_requests = None loop._presentation_manager = Mock() loop._presentation_manager.presented_frames.return_value = () @@ -105,6 +109,12 @@ def test_taxi_ui_requests_fullscreen_dimensions_before_resizing( assert (loop.state.width, loop.state.height) == (1920, 1080) assert loop.flush_ui_loop_requests() is None + loop.state._open_options() + loop.state._save_options() + assert loop.state.presentation_size == (1920, 1080) + assert "width: 1920" in document.path.read_text(encoding="utf-8") + assert "height: 1080" in document.path.read_text(encoding="utf-8") + loop.state.presentation_size = (640, 480) loop.step_ui(object(), 3, UserInputEvents([])) requests = loop.flush_ui_loop_requests() diff --git a/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/game_physics.py b/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/game_physics.py index 2c1d20a75..5819ae613 100644 --- a/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/game_physics.py +++ b/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/game_physics.py @@ -250,7 +250,7 @@ def _reinforce_static_barrier_rebound( position = np.asarray(requested_ego.position_m[:2], dtype=np.float32) incoming_velocity = np.asarray(requested_ego.linear_velocity_mps, dtype=np.float32) reinforced = np.asarray(resolved_velocity_mps, dtype=np.float32).copy() - if len(barriers.starts_xy_m) == 0: + if len(barriers.starts_xy_m) == 0 or not np.any(incoming_velocity[:2]): return reinforced, False yaw = _yaw_from_quaternion_xyzw(requested_ego.orientation_xyzw) diff --git a/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/ground_snap.py b/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/ground_snap.py index 6cd0feff4..eb87b1c41 100644 --- a/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/ground_snap.py +++ b/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/ground_snap.py @@ -60,6 +60,8 @@ def __init__( self._min_intersections = int(min_intersections) self._anchor_offset_m: float | None = None self._invalid_sample_handler = invalid_sample_handler + self._body_grid_key: tuple[float, float, float, int] | None = None + self._body_grid: np.ndarray | None = None vertices_d = np.asarray(vertices_xyz, dtype=np.float64) faces_i = np.asarray(faces_ijk, dtype=np.int32) @@ -117,51 +119,77 @@ def _cell_y(self, y: float) -> int: j = int((y - self._grid_origin[1]) / self._grid_resolution_m) return max(0, min(self._grid_shape[1] - 1, j)) - def _ground_z_at(self, x: float, y: float, z_ref: float) -> float | None: - if ( - x < self._grid_origin[0] - or y < self._grid_origin[1] - or x > self._grid_origin[0] + self._grid_shape[0] * self._grid_resolution_m - or y > self._grid_origin[1] + self._grid_shape[1] * self._grid_resolution_m - ): - return None - candidates = self._cell_candidates.get((self._cell_x(x), self._cell_y(y))) - if candidates is None or candidates.size == 0: - return None - v2x = x - self._a_x[candidates] - v2y = y - self._a_y[candidates] - v0y_c = self._v0y[candidates] - v1x_c = self._v1x[candidates] - v0x_c = self._v0x[candidates] - v1y_c = self._v1y[candidates] - inv = self._inv_denom[candidates] - v = (v2x * v1y_c - v1x_c * v2y) * inv - w = (v0x_c * v2y - v2x * v0y_c) * inv - u = 1.0 - v - w - eps = 1e-6 - inside = self._denom_valid[candidates] & (u >= -eps) & (v >= -eps) & (w >= -eps) - if not bool(inside.any()): - return None - z = ( - u * self._a_z[candidates] - + v * self._b_z[candidates] - + w * self._c_z[candidates] - ) - z_inside = z[inside] - best = int(np.argmin(np.abs(z_inside - z_ref))) - return float(z_inside[best]) + def _raycast(self, points: np.ndarray) -> np.ndarray: + """Cast vertical rays in batches sharing the same triangle candidates.""" + result = np.full(len(points), np.nan, dtype=np.float64) + groups: dict[tuple[int, int], list[int]] = {} + for index, (x, y, _) in enumerate(points): + if ( + x < self._grid_origin[0] + or y < self._grid_origin[1] + or x + > self._grid_origin[0] + self._grid_shape[0] * self._grid_resolution_m + or y + > self._grid_origin[1] + self._grid_shape[1] * self._grid_resolution_m + ): + continue + groups.setdefault((self._cell_x(x), self._cell_y(y)), []).append(index) + # ponytail: matrices scale with rays per cell times candidate triangles; + # tile ray groups if sample grids grow beyond the default 16 rays. + for cell, indices in groups.items(): + candidates = self._cell_candidates.get(cell) + if candidates is None or candidates.size == 0: + continue + selected = points[indices] + v2x = selected[:, 0, None] - self._a_x[candidates] + v2y = selected[:, 1, None] - self._a_y[candidates] + v = ( + v2x * self._v1y[candidates] - self._v1x[candidates] * v2y + ) * self._inv_denom[candidates] + w = ( + self._v0x[candidates] * v2y - v2x * self._v0y[candidates] + ) * self._inv_denom[candidates] + u = 1.0 - v - w + inside = ( + self._denom_valid[candidates] + & (u >= -1e-6) + & (v >= -1e-6) + & (w >= -1e-6) + ) + z = ( + u * self._a_z[candidates] + + v * self._b_z[candidates] + + w * self._c_z[candidates] + ) + best = np.argmin( + np.where(inside, np.abs(z - selected[:, 2, None]), np.inf), axis=1 + ) + hit = inside.any(axis=1) + result[np.asarray(indices)[hit]] = z[np.flatnonzero(hit), best[hit]] + return result def _sample_body_grid(self, vehicle: VehicleConfig) -> np.ndarray: + key = ( + vehicle.aabb_length_m, + vehicle.aabb_width_m, + vehicle.aabb_height_m, + self._num_sample_points, + ) + if key == self._body_grid_key: + assert self._body_grid is not None + return self._body_grid n = max(2, int(np.ceil(self._num_sample_points**0.5))) p = np.linspace(0.0, 1.0, n) u, v = np.meshgrid(p, p) - return np.column_stack( + self._body_grid = np.column_stack( [ u.ravel() * vehicle.aabb_length_m - vehicle.aabb_length_m * 0.5, v.ravel() * vehicle.aabb_width_m - vehicle.aabb_width_m * 0.5, np.full(u.size, -vehicle.aabb_height_m * 0.5), ] ) + self._body_grid_key = key + return self._body_grid def snap(self, state: VehicleState, vehicle: VehicleConfig) -> VehicleState: body_pts = self._sample_body_grid(vehicle) @@ -169,10 +197,7 @@ def snap(self, state: VehicleState, vehicle: VehicleConfig) -> VehicleState: world_pts = body_pts @ rot.T + np.array( [state.x_m, state.y_m, state.z_m], dtype=np.float64 ) - ground_zs = np.array( - [self._raycast(float(p[0]), float(p[1]), float(p[2])) for p in world_pts], - dtype=np.float64, - ) + ground_zs = self._raycast(world_pts) mask = ~np.isnan(ground_zs) n_hits = int(mask.sum()) n_total = int(len(world_pts)) @@ -235,10 +260,6 @@ def _handle_invalid_sample(self, state: VehicleState) -> VehicleState: return state return self._invalid_sample_handler(state) - def _raycast(self, x: float, y: float, z_ref: float) -> float: - z = self._ground_z_at(x, y, z_ref) - return float("nan") if z is None else z - class _InsufficientPoints(Exception): pass diff --git a/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/map_traffic.py b/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/map_traffic.py index afbdd13a7..a44e0f417 100644 --- a/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/map_traffic.py +++ b/apps/omnidreams_game_engine/omnidreams_game_engine/simulation/map_traffic.py @@ -303,12 +303,36 @@ def _drive_target_timestamp_us(state: MapTrafficVehicleState) -> int: return int((state.timestamp_us + lookahead_us) % state.duration_us) def _observation(self, state: MapTrafficVehicleState) -> _TrafficObservation: + track = state.scene_object + timestamp_us = int(state.timestamp_us) + index = int(np.searchsorted(track.timestamps_us, timestamp_us, side="left")) + if index <= 0: + lo = hi = 0 + alpha = 0.0 + elif index >= len(track.timestamps_us): + lo = hi = len(track.timestamps_us) - 1 + alpha = 0.0 + else: + lo, hi = index - 1, index + span = int(track.timestamps_us[hi] - track.timestamps_us[lo]) + alpha = ( + 0.0 if span == 0 else (timestamp_us - track.timestamps_us[lo]) / span + ) + if lo == hi: + velocity = np.zeros(3, dtype=np.float32) + else: + dt_s = ( + float(track.timestamps_us[hi] - track.timestamps_us[lo]) / 1_000_000.0 + ) + velocity = ((track.positions_m[hi] - track.positions_m[lo]) / dt_s).astype( + np.float32 + ) if state.object_id in self._active_ids: position = state.position_m[:2] - _, _, velocity = state.scene_object.sample(int(state.timestamp_us)) else: - position, _, velocity = state.scene_object.sample(int(state.timestamp_us)) - position = position[:2] + position = ( + track.positions_m[lo] * (1.0 - alpha) + track.positions_m[hi] * alpha + ).astype(np.float32)[:2] return _TrafficObservation( position_xy=np.asarray(position, dtype=np.float32), velocity_xy=np.asarray(velocity[:2], dtype=np.float32), @@ -380,26 +404,22 @@ def _nearest_local_route_projection( (best.segment_index - 1) % segment_count, (best.segment_index + 1) % segment_count, ) - neighbors = tuple( - cls._route_projection(state, position_xy, segment_index) - for segment_index in neighbor_indices - if segment_index not in visited - ) - visited.update(projection.segment_index for projection in neighbors) - better = tuple( - projection - for projection in neighbors - if cls._projection_is_better(projection, best) - ) - if not better: - break - best = min( - better, - key=lambda projection: ( + next_best = None + for segment_index in neighbor_indices: + if segment_index in visited: + continue + projection = cls._route_projection(state, position_xy, segment_index) + visited.add(segment_index) + if not cls._projection_is_better(projection, best): + continue + if next_best is None or ( projection.distance_sq, projection.progress_distance, - ), - ) + ) < (next_best.distance_sq, next_best.progress_distance): + next_best = projection + if next_best is None: + break + best = next_best return best @classmethod @@ -510,41 +530,40 @@ def _headway_scale( observation: _TrafficObservation, candidates: tuple[_TrafficObservation, ...], ) -> float: - velocity = observation.velocity_xy - speed_mps = float(np.linalg.norm(velocity[:2])) + vx, vy = (float(value) for value in observation.velocity_xy) + speed_mps = math.hypot(vx, vy) if speed_mps <= 1.0e-4: return 0.0 - forward = velocity[:2] / speed_mps + forward_x, forward_y = vx / speed_mps, vy / speed_mps + position_x, position_y = (float(value) for value in observation.position_xy) + minimum_heading_dot = math.cos(_MAX_HEADING_DELTA_RAD) + desired_clearance = _MIN_CLEARANCE_M + _TIME_HEADWAY_S * speed_mps best_clearance = math.inf for other in candidates: if other is observation: continue - delta = other.position_xy - observation.position_xy - longitudinal = float(np.dot(delta, forward)) + delta_x = float(other.position_xy[0]) - position_x + delta_y = float(other.position_xy[1]) - position_y + longitudinal = delta_x * forward_x + delta_y * forward_y if longitudinal <= 0.0: continue - lateral = abs(float(forward[0] * delta[1] - forward[1] * delta[0])) + lateral = abs(forward_x * delta_y - forward_y * delta_x) if lateral > _LANE_CORRIDOR_M: continue - other_speed = float(np.linalg.norm(other.velocity_xy)) + other_vx, other_vy = (float(value) for value in other.velocity_xy) + other_speed = math.hypot(other_vx, other_vy) if other_speed > 1.0e-4: - other_heading = other.velocity_xy / other_speed - angle = math.acos( - float(np.clip(np.dot(forward, other_heading), -1.0, 1.0)) - ) - if angle > _MAX_HEADING_DELTA_RAD: + heading_dot = ( + forward_x * other_vx + forward_y * other_vy + ) / other_speed + if heading_dot < minimum_heading_dot: continue clearance = longitudinal - observation.half_length_m - other.half_length_m + if clearance <= desired_clearance: + return 0.0 best_clearance = min(best_clearance, clearance) - desired_clearance = _MIN_CLEARANCE_M + _TIME_HEADWAY_S * speed_mps - if best_clearance <= desired_clearance: - return 0.0 - return float( - np.clip( - (best_clearance - desired_clearance) / _BRAKING_MARGIN_M, - 0.0, - 1.0, - ) + return min( + max((best_clearance - desired_clearance) / _BRAKING_MARGIN_M, 0.0), 1.0 ) def prepare_topology(self, ego: BodyState) -> None: diff --git a/apps/t2v/tests/test_structure.py b/apps/t2v/tests/test_structure.py deleted file mode 100644 index 12a85b779..000000000 --- a/apps/t2v/tests/test_structure.py +++ /dev/null @@ -1,123 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Repository-layout checks for T2V application ownership.""" - -from pathlib import Path - -import pytest -import tomli as tomllib - -pytestmark = pytest.mark.ci_cpu - -_REPO_ROOT = Path(__file__).resolve().parents[3] - -_MODEL_APPLICATIONS = ( - ( - "causal_forcing", - { - "t2v-causal-forcing-wan2.1-t2v-1.3b-chunkwise": "create_app", - "t2v-causal-forcing-wan2.1-t2v-1.3b-framewise": "create_app_framewise", - }, - ), - ( - "cosmos_predict2", - {"t2v-cosmos2-t2v-2b-720p": "create_app"}, - ), - ( - "fastvideo_causal_wan22", - {"t2v-fastvideo-causal-wan2.2-t2v-14b": "create_app"}, - ), - ( - "self_forcing", - { - "t2v-self-forcing-wan2.1-t2v-1.3b": "create_app", - "t2v-self-forcing-wan2.1-t2v-1.3b-taehv": "create_app_taehv", - "t2v-self-forcing-wan2.1-t2v-1.3b-sink5-window7-rerope": ( - "create_app_sink5_window7_rerope" - ), - }, - ), - ("wan21", {"t2v-wan21-t2v-1.3b-480p": "create_app"}), - ("wan22", {"t2v-wan22-ti2v-5b": "create_app"}), -) -"""Model packages and their user-facing application slugs.""" - - -@pytest.mark.parametrize(("model", "factories"), _MODEL_APPLICATIONS) -def test_model_owns_v2_t2v_adapter(model: str, factories: dict[str, str]) -> None: - """Keep model code and its T2V adapter in one v2 integration package.""" - project = _REPO_ROOT / "integrations_v2" / model - application_project = project / "apps" / "t2v" - implementation = project / "impl" - config = project / "config.py" - manifest = tomllib.loads((project / "pyproject.toml").read_text()) - - assert not (_REPO_ROOT / "integrations" / model).exists() - assert {path.name for path in project.glob("*.py")} == {"__init__.py", "config.py"} - assert not (project / "runner.py").exists() - assert {path.name for path in application_project.glob("*.py")} == { - "__init__.py", - "adapter.py", - } - assert not (application_project / "t2v").exists() - assert config.is_file() - assert (implementation / "__init__.py").is_file() - assert not (implementation / "config.py").exists() - for source_path in implementation.rglob("*.py"): - source = source_path.read_text() - assert "from t2v" not in source - assert "import t2v" not in source - assert "flashdreams.runtime_v2" not in source - assert "flashdreams.serving" not in source - assert not list(project.glob("apps/*/config.py")) - application_readme = (application_project / "README.md").read_text() - model_readme = (project / "README.md").read_text() - for slug in factories: - assert slug in application_readme - assert slug in model_readme - assert "../../../../apps/t2v/README.md" in application_readme - assert not (application_project / "tests").exists() - assert not (application_project / "impl").exists() - assert (project / "tests").is_dir() - assert "flashdreams-t2v" in manifest["project"]["dependencies"] - assert f"{model}.impl" in manifest["tool"]["setuptools"]["packages"] - for slug, factory in factories.items(): - assert factory == "create_app" or ( - factory.startswith("create_app_") - and slug.endswith( - "-" + factory.removeprefix("create_app_").replace("_", "-") - ) - ) - assert manifest["tool"]["setuptools"]["package-dir"] == {model: "."} - assert manifest["project"]["entry-points"]["flashdreams.applications_v2"] == { - slug: f"{model}.apps.t2v.adapter:{factory}" - for slug, factory in factories.items() - } - assert "flashdreams.runner_configs" not in manifest["project"]["entry-points"] - assert "RUNNER_" not in config.read_text() - - -def test_shared_t2v_code_is_owned_by_app_package() -> None: - """Keep reusable demo infrastructure outside the framework and models.""" - project = _REPO_ROOT / "apps" / "t2v" - package = project / "t2v" - manifest = tomllib.loads((project / "pyproject.toml").read_text()) - - assert {path.name for path in project.glob("*.py")} == set() - assert {path.name for path in package.glob("*.py")} == { - "__init__.py", - "application.py", - "defaults.py", - "session.py", - "testing.py", - "ui.py", - } - assert (project / "tests").is_dir() - readme = (project / "README.md").read_text() - for heading in ("## Controls", "## Usage", "## Tests"): - assert heading in readme - assert manifest["tool"]["setuptools"]["packages"]["find"]["include"] == ["t2v*"] - assert not (_REPO_ROOT / "flashdreams" / "flashdreams" / "t2v_v2").exists() - for model, _factories in _MODEL_APPLICATIONS: - assert not (_REPO_ROOT / "integrations_v2" / f"t2v_{model}").exists() diff --git a/docs/source/documentation/tools/offline_program_packager.md b/docs/source/documentation/tools/offline_program_packager.md index ae4b5a517..f94943ee7 100644 --- a/docs/source/documentation/tools/offline_program_packager.md +++ b/docs/source/documentation/tools/offline_program_packager.md @@ -7,7 +7,7 @@ title: 'Offline Program Packager' `tools/package-as-offline-exe/package_as_offline_exe.py` preloads an installed FlashDreams v2 application and creates a host-native executable with its Python -runtime, dependencies, native libraries, and prepared caches in one folder. +runtime, dependencies, native libraries, and prepared caches in one raw folder. The target machine does not need Python, `uv`, or network access. The packager supports `native-window` and `webrtc` applications. It is not a @@ -20,35 +20,10 @@ Run the tool from the repository root. Use `:::` to separate packager options from the complete `flashdreams-run-v2` command: ```bash - -uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ - --output artifacts/interactive-drive-omnidreams-bundle \ - ::: flashdreams-run-v2 interactive-drive-omnidreams \ - --mode native-window - -``` - -For a browser application, package its normal WebRTC command: - -```bash - uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ - --output artifacts/interactive-drive-omnidreams-webrtc-bundle \ - ::: flashdreams-run-v2 interactive-drive-omnidreams \ - --mode webrtc --host 0.0.0.0 --port 8089 - -``` - -Arguments before the runtime command's `--` belong to FlashDreams. Arguments -after it belong to the application and are embedded in the generated launcher: - -```bash - -uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ - ::: flashdreams-run-v2 interactive-drive-omnidreams \ - --mode native-window --timeout 120 \ - -- --game-mode - + --output artifacts/taxi-omnidreams-bundle \ + ::: flashdreams-run-v2 crazy-robotaxi-omnidreams-fast-perf \ + --mode native-window --fullscreen ``` Do not pass `--preload-application`; the packager controls preparation and @@ -64,6 +39,9 @@ validation. If `--output` is omitted, the destination is | `--skip-runtime-validation` | Initialize the application without validating one model block. | | `--hidden-import MODULE` | Add a PyInstaller hidden import; repeat as needed. | | `--collect-executable NAME` | Include an executable found on `PATH`; repeat as needed. | +| `--noconsole` | Build without a console window on Windows (ignored on Linux). `stdout` and `stderr` write to `%TEMP%\flashdreams\\runtime.log`. | +| +| `--working-dir PATH` | Defaults to the output directory (which must not already exist. Creates dir if missing and copy its contents into the bundle before preload. | Use `--help` for the authoritative option list: @@ -79,32 +57,41 @@ uv run python tools/package-as-offline-exe/package_as_offline_exe.py --help / |-- [.exe] -|-- data/ +|-- _internal/ |-- cache/ +|-- artifacts/ # when preload produces application assets |-- INSTALLER_OUTPUT.txt |-- PREPARATION_ISSUES.txt # only when preload warnings are found `-- README.md ``` -`data/` contains the executable runtime. `cache/` contains the resources and +The executable embeds Python bytecode, which is read without extraction. +`_internal/` contains the executable runtime. `cache/` contains the resources and build artifacts prepared during initialization. `INSTALLER_OUTPUT.txt` records preload, validation, and PyInstaller output. `PREPARATION_ISSUES.txt` records work that the application attempted after its preparation phase. Distribute the complete folder, not only the executable: the launcher depends -on its sibling `data/` and `cache/` directories. +on its sibling `_internal/` and `cache/` directories. The packager uses +PyInstaller's directory mode. Embedded bytecode and the standard-library archive +are read directly. Python and DLLs are not extracted into temporary storage at +launch, and the cache is not archived. +Deploy the complete folder as raw files. ## Runtime cache The bundled cache is a read-only seed. On first launch, the application copies it to a writable per-user cache: -- Windows: `%LOCALAPPDATA%\FlashDreams\\cache` -- Linux: `$XDG_CACHE_HOME/flashdreams/`, or - `~/.cache/flashdreams/` when `XDG_CACHE_HOME` is unset +- Windows: `%USERPROFILE%\flashdreams\unpacked\\cache` +- Linux: `~/flashdreams/unpacked//cache` -Set `FLASHDREAMS_RUNTIME_CACHE_DIR` to choose another writable cache location. +Pass `--unpack-dir PATH` to use `PATH//cache`, or set +`FLASHDREAMS_RUNTIME_CACHE_DIR` to choose the exact writable cache location. +The command-line option takes precedence; paths must be absolute. +Prepared C++, Triton, and PhysX extensions load from the original bundled `cache/`; +Python and runtime libraries load from `_internal/`. Before distributing a bundle, test it on a clean target machine with networking disabled and empty user caches. Also confirm that every bundled model, asset, diff --git a/flashdreams/flashdreams/core/checkpoint/load.py b/flashdreams/flashdreams/core/checkpoint/load.py index 2d02da91d..c1e582570 100644 --- a/flashdreams/flashdreams/core/checkpoint/load.py +++ b/flashdreams/flashdreams/core/checkpoint/load.py @@ -60,16 +60,16 @@ def _preflight_hf_cache( def _hf_cache_filename(filename: str, subfolder: str | None) -> str: - return f"{subfolder.rstrip('/')}/{filename}" if subfolder else filename + return os.path.join(*subfolder.split("/"), filename) if subfolder else filename -def _is_hf_file_cached( +def _cached_hf_file_path( *, repo_id: str, filename: str, subfolder: str | None, revision: str, -) -> bool: +) -> str | None: try: cached = try_to_load_from_cache( repo_id=repo_id, @@ -77,8 +77,8 @@ def _is_hf_file_cached( revision=revision, ) except Exception: - return False - return isinstance(cached, str) and os.path.exists(cached) + return None + return cached if isinstance(cached, str) and os.path.isfile(cached) else None def _preflight_checkpoint_cache_requirement( @@ -493,23 +493,27 @@ def _download_checkpoint_from_huggingface_url( ) -> str: """Download a checkpoint from Hugging Face and return local cached path.""" repo_id, filename, subfolder, revision = _parse_huggingface_checkpoint_url(url) - logger.info(f"Downloading checkpoint from Hugging Face: {url}") settings: dict[str, object] = { "repo": repo_id, "filename": filename, "revision": revision, } - if not _is_hf_file_cached( + cached_path = _cached_hf_file_path( repo_id=repo_id, filename=filename, subfolder=subfolder, revision=revision, - ): - _preflight_checkpoint_cache_requirement( - label="Hugging Face checkpoint cache", - min_free_gb=checkpoint_min_free_gb, - settings=settings, - ) + ) + if cached_path is not None: + logger.info(f"Loading checkpoint from local HF cache: {cached_path}") + return cached_path + + logger.info(f"Downloading checkpoint from Hugging Face: {url}") + _preflight_checkpoint_cache_requirement( + label="Hugging Face checkpoint cache", + min_free_gb=checkpoint_min_free_gb, + settings=settings, + ) min_bytes = _preflight_hf_cache( label="Hugging Face checkpoint cache", settings=settings, diff --git a/flashdreams/test_v2/test_webrtc_client_window.py b/flashdreams/test_v2/test_webrtc_client_window.py index fe0873dba..8300f4f97 100644 --- a/flashdreams/test_v2/test_webrtc_client_window.py +++ b/flashdreams/test_v2/test_webrtc_client_window.py @@ -888,7 +888,6 @@ async def test_server_closes_while_browser_peer_is_still_connected() -> None: window = WebRTCClientWindow(startup_timeout_seconds=1.0) peer: RTCPeerConnection | None = None try: - assert isinstance(window.server._loop, asyncio.SelectorEventLoop) window.open(_session_desc()) peer, _, _ = await _connect_browser(window) diff --git a/flashdreams/tests/test_benchmark_harness.py b/flashdreams/tests/test_benchmark_harness.py index 87f62d03f..0d3e8e0fc 100644 --- a/flashdreams/tests/test_benchmark_harness.py +++ b/flashdreams/tests/test_benchmark_harness.py @@ -1824,154 +1824,6 @@ def test_pai_bench_upstream_runner_warns_on_aarch64( assert "decord" in warnings[0] -def test_pai_bench_preflight_reports_missing_import(tmp_path: Path) -> None: - video = tmp_path / "candidate.mp4" - video.write_bytes(b"fake mp4") - pai_root = tmp_path / "physical-ai-bench" - generation_dir = pai_root / "generation" - pbench_dir = generation_dir / "pbench" - pbench_dir.mkdir(parents=True) - (generation_dir / "evaluate.py").write_text( - "raise AssertionError('evaluator should not launch after preflight failure')\n", - encoding="utf-8", - ) - (pbench_dir / "__init__.py").write_text("", encoding="utf-8") - (pbench_dir / "aesthetic_quality.py").write_text( - "import missing_clip_for_test\n", - encoding="utf-8", - ) - - result = pai_bench_profile.main( - [ - "--profile", - "pai-bench-g", - "--video", - str(video), - "--scenario-id", - "demo-scenario", - "--quality-dir", - str(tmp_path / "quality"), - "--output", - str(tmp_path / "quality" / "metrics.json"), - "--pai-bench-root", - str(pai_root), - "--no-fetch", - "--dimensions", - "aesthetic_quality", - ] - ) - - assert result == 1 - payload = json.loads((tmp_path / "quality" / "metrics.json").read_text()) - assert payload["status"] == "fail" - assert payload["metrics"] == {} - assert payload["metadata"]["preflight"]["returncode"] == 1 - assert ( - payload["metadata"]["preflight"]["payload"]["failures"][0]["missing_import"] - == "missing_clip_for_test" - ) - assert "missing Python import 'missing_clip_for_test'" in payload["warnings"][0] - assert (tmp_path / "quality" / "pai_bench_preflight.log").is_file() - assert not (tmp_path / "quality" / "pai_bench_command.log").exists() - assert not (tmp_path / "quality" / "staged" / "videos").exists() - - -def test_pai_bench_profile_normalizes_public_evaluator_output( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - video = tmp_path / "candidate.mp4" - video.write_bytes(b"fake mp4") - pai_root = tmp_path / "physical-ai-bench" - generation_dir = pai_root / "generation" - generation_dir.mkdir(parents=True) - pbench_dir = generation_dir / "pbench" - pbench_dir.mkdir() - (pbench_dir / "__init__.py").write_text("", encoding="utf-8") - (pbench_dir / "motion_smoothness.py").write_text("", encoding="utf-8") - (pbench_dir / "imaging_quality.py").write_text("", encoding="utf-8") - fake_runner = generation_dir / "evaluate.py" - fake_runner.write_text( - "import json, sys\n" - "from pathlib import Path\n" - "out = Path(sys.argv[sys.argv.index('--output_path') + 1])\n" - "out.mkdir(parents=True, exist_ok=True)\n" - "payload = {\n" - " 'motion_smoothness': [0.8, [{'video_path': 'demo_000.mp4', 'video_results': 0.8}]],\n" - " 'imaging_quality': [0.82, [{'video_path': 'demo_000.mp4', 'video_results': 82.0}]],\n" - "}\n" - "(out / 'fake_eval_results.json').write_text(json.dumps(payload))\n", - encoding="utf-8", - ) - fake_python = tmp_path / "fake_python" - fake_python.write_text( - "#!/usr/bin/env python3\n" - "import os, runpy, sys\n" - "if len(sys.argv) > 2 and sys.argv[1] == '-c':\n" - " code = sys.argv[2]\n" - " sys.argv = [sys.argv[0], *sys.argv[3:]]\n" - " exec(code)\n" - " raise SystemExit(0)\n" - "assert 'FLASHDREAMS_PAI_BENCH_LOCAL_RUNNER' in os.environ\n" - "import decord\n" - "assert decord.VideoReader is not None\n" - "script_index = next(\n" - " index for index, arg in enumerate(sys.argv)\n" - " if arg.endswith('evaluate.py')\n" - ")\n" - "sys.argv = [sys.argv[script_index], *sys.argv[script_index + 1:]]\n" - "runpy.run_path(sys.argv[0], run_name='__main__')\n", - encoding="utf-8", - ) - fake_python.chmod(0o755) - - result = pai_bench_profile.main( - [ - "--profile", - "pai-bench-g", - "--video", - str(video), - "--scenario-id", - "demo-scenario", - "--prompt", - "Demo scenario", - "--quality-dir", - str(tmp_path / "quality"), - "--output", - str(tmp_path / "quality" / "metrics.json"), - "--pai-bench-root", - str(pai_root), - "--python", - str(fake_python), - "--no-fetch", - "--dimensions", - "motion_smoothness", - "imaging_quality", - ] - ) - - assert result == 0 - payload = json.loads((tmp_path / "quality" / "metrics.json").read_text()) - metrics = payload["metrics"] - assert payload["status"] == "pass" - assert payload["metadata"]["runner"] == "local" - assert ( - "FLASHDREAMS_PAI_BENCH_LOCAL_RUNNER" - in payload["metadata"]["command_manifest"]["env"] - ) - assert metrics["pai_bench_g_motion_smoothness_score"] == pytest.approx(80.0) - assert metrics["pai_bench_g_imaging_quality_score"] == pytest.approx(82.0) - assert metrics["pai_bench_g_score"] == pytest.approx(81.0) - assert metrics["pai_bench_g_videos_evaluated"] == 1 - prompt_payload = json.loads( - (tmp_path / "quality" / "staged" / "prompt_file.json").read_text() - ) - assert prompt_payload == [ - {"video_id": "demo-scenario", "prompt_en": "Demo scenario"} - ] - assert not (tmp_path / "quality" / "staged" / "videos").exists() - - def test_pai_bench_decord_compat_reads_video_batches(tmp_path: Path) -> None: cv2 = pytest.importorskip("cv2") torch = pytest.importorskip("torch") diff --git a/flashdreams/tests/test_checkpoint_loading.py b/flashdreams/tests/test_checkpoint_loading.py index 1e51dd95a..6b50a0839 100644 --- a/flashdreams/tests/test_checkpoint_loading.py +++ b/flashdreams/tests/test_checkpoint_loading.py @@ -16,6 +16,7 @@ import pytest import torch +from huggingface_hub import constants as hf_constants from safetensors.torch import save_file as save_safetensors_file pytestmark = pytest.mark.ci_cpu @@ -34,6 +35,35 @@ def __reduce__(self) -> tuple[Any, tuple[str]]: return _record_object_load, (self.marker_path,) +def test_cached_hf_subfolder_needs_no_free_space( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + checkpoint_load = importlib.import_module("flashdreams.core.checkpoint.load") + cache_root = tmp_path / "hub" + repo_cache = cache_root / "models--org--model" + sha = "a" * 40 + (repo_cache / "refs").mkdir(parents=True) + (repo_cache / "refs" / "main").write_text(sha, encoding="utf-8") + checkpoint = repo_cache / "snapshots" / sha / "single_view" / "weights.pt" + checkpoint.parent.mkdir(parents=True) + checkpoint.write_bytes(b"cached") + cache_dir = f"\\\\?\\{cache_root}" if os.name == "nt" else str(cache_root) + monkeypatch.setattr(hf_constants, "HF_HUB_CACHE", cache_dir) + + def reject_preflight(*_args: Any, **_kwargs: Any) -> None: + pytest.fail("cached checkpoints must not require free disk") + + monkeypatch.setattr(checkpoint_load, "ensure_free_disk", reject_preflight) + monkeypatch.setattr(checkpoint_load, "hf_hub_download", reject_preflight) + + cached = checkpoint_load._download_checkpoint_from_huggingface_url( + "https://huggingface.co/org/model/resolve/main/single_view/weights.pt", + checkpoint_min_free_gb=200, + ) + + assert Path(cached).samefile(checkpoint) + + @pytest.mark.parametrize("source", ["huggingface", "s3", "distributed-cache"]) def test_checkpoint_loads_reject_non_weight_objects( source: str, diff --git a/flashdreams/tests/test_multiview_mask.py b/flashdreams/tests/test_multiview_mask.py index 6c8a41a3c..39b31488f 100644 --- a/flashdreams/tests/test_multiview_mask.py +++ b/flashdreams/tests/test_multiview_mask.py @@ -17,7 +17,6 @@ ROLE_UND, StreamFields, _compiled_create_block_mask, - build_block_mask, visibility, visibility_mask_mod, ) @@ -293,11 +292,3 @@ def test_the_block_mask_builder_compiles_once() -> None: cost more than the kernel saves. """ assert _compiled_create_block_mask() is _compiled_create_block_mask() - - -def test_the_block_mask_builder_accepts_asymmetric_tiles() -> None: - stream = build_stream(_sample_tokens()) - - mask = build_block_mask(stream, stream, block_size=(16, 32)) - - assert mask.BLOCK_SIZE == (16, 32) diff --git a/flashdreams/tests/test_multiview_prefill.py b/flashdreams/tests/test_multiview_prefill.py index 7af4c3b4a..c6d7010e5 100644 --- a/flashdreams/tests/test_multiview_prefill.py +++ b/flashdreams/tests/test_multiview_prefill.py @@ -198,15 +198,6 @@ def test_every_prefill_query_can_reach_a_key() -> None: assert bool(mask.any(dim=1).all()) -def test_prefill_block_mask_accepts_asymmetric_tiles() -> None: - pack = build_prefill_pack(CLIP, text_tokens=TEXT_TOKENS) - - mask = pack.block_mask(block_size=(16, 32)) - - assert tuple(mask.shape[-2:]) == (len(pack.gen), pack.num_und + len(pack.gen)) - assert mask.BLOCK_SIZE == (16, 32) - - def test_control_queries_cannot_see_the_target_item() -> None: pack = build_prefill_pack( CLIP, diff --git a/flashdreams/tests/test_multiview_rollout.py b/flashdreams/tests/test_multiview_rollout.py index 0760b1f40..5a330f9c2 100644 --- a/flashdreams/tests/test_multiview_rollout.py +++ b/flashdreams/tests/test_multiview_rollout.py @@ -10,7 +10,6 @@ import pytest import torch from torch import Tensor -from torch.nn.attention.flex_attention import BlockMask import flashdreams.core.attention.multiview.rollout as rollout_module from flashdreams.core.attention.multiview import ( @@ -260,26 +259,6 @@ def test_view_caption_and_position_options_cross_the_rollout_seam() -> None: assert first_mask[:, :2].tolist() == [[True, False], [False, True]] -def test_block_mask_selection_crosses_the_protocol_boundary() -> None: - """Supply the same visibility contract with the requested block geometry.""" - model = _Model() - controls, text_ids, condition = _inputs() - run_rollout( - model, - geometry=GEOMETRY, - controls=controls, - text_ids=text_ids, - condition_tokens=condition, - use_block_mask=True, - mask_block_size=(16, 32), - ) - - assert model.state.masks - for mask in model.state.masks: - assert isinstance(mask, BlockMask) - assert mask.BLOCK_SIZE == (16, 32) - - def test_seeded_rollouts_are_reproducible_through_an_adapter() -> None: """Keep random sampling deterministic independently of model implementation.""" controls, text_ids, condition = _inputs() diff --git a/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_cpp/render/ludus_cuda.cu b/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_cpp/render/ludus_cuda.cu index b784e9738..8e1ab8dd6 100644 --- a/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_cpp/render/ludus_cuda.cu +++ b/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_cpp/render/ludus_cuda.cu @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #define CUDA_CHECK(CALL) do { cudaError_t err = CALL; if (err != cudaSuccess) { fprintf(stderr, "CUDA error %d (%s) at %s:%d: %s\n", (int)err, cudaGetErrorString(err), __FILE__, __LINE__, #CALL); } } while(0) @@ -2497,6 +2499,8 @@ void ludusCudaInit(NVDR_CTX_ARGS, LudusCudaState& s) { memset(&s, 0, sizeof(LudusCudaState)); s.cr = new CR::CudaRaster(); + // Keep main and BEV surfaces separate instead of reallocating on every switch. + s.crBev = new CR::CudaRaster(); s.tessellationThreshold = 0.0f; s.depthScaling = 1.0f; s.cullRadiusScale = 1.5f; @@ -2507,13 +2511,13 @@ void ludusCudaInit(NVDR_CTX_ARGS, LudusCudaState& s) CUDA_CHECK(cudaMalloc(&s.atomicVertexCount, 3 * sizeof(int))); CUDA_CHECK(cudaMalloc(&s.atomicTriangleCount, 2 * sizeof(int))); - printf("LudusCuda: Initialized renderer\n"); } void ludusCudaDestroy(NVDR_CTX_ARGS, LudusCudaState& s) { if (s.cr) { delete s.cr; s.cr = nullptr; } + if (s.crBev) { delete s.crBev; s.crBev = nullptr; } if (s.projectedVertices) cudaFree(s.projectedVertices); if (s.triangleIndices) cudaFree(s.triangleIndices); if (s.vertexColors) cudaFree(s.vertexColors); @@ -2740,7 +2744,9 @@ void ludusCudaRender( int crHeight = (ssH + 7) & ~7; int maxVp = (crWidth > crHeight) ? crWidth : crHeight; - s.cr->setBufferSize(crWidth, crHeight, 1); + if (!s.crBev || s.cr->getBufferWidth() != crWidth || + s.cr->getBufferHeight() != crHeight || s.cr->getNumImages() != 1) + s.cr->setBufferSize(crWidth, crHeight, 1); // Allocate hi-res intermediate buffer for SSAA if (smoothed) { @@ -2963,6 +2969,7 @@ void ludusCudaRenderTimestamped( uint8_t* outputPtr) { if (numCameras == 0 || width == 0 || height == 0) return; + CR::CudaRaster* cr = s.crBev && params.cameraTypeId == 1 ? s.crBev : s.cr; // Pool headers stay device-resident; totalCubes is cached at scene upload. bool hasTess = params.tessellationThreshold > 0.0f; @@ -3000,7 +3007,9 @@ void ludusCudaRenderTimestamped( int crWidth = (ssW + 7) & ~7; int crHeight = (ssH + 7) & ~7; int maxVp = (crWidth > crHeight) ? crWidth : crHeight; - s.cr->setBufferSize(crWidth, crHeight, 1); + if (!s.crBev || cr->getBufferWidth() != crWidth || + cr->getBufferHeight() != crHeight || cr->getNumImages() != 1) + cr->setBufferSize(crWidth, crHeight, 1); // Allocate hi-res intermediate buffer for SSAA if (smoothed) { @@ -3103,11 +3112,11 @@ void ludusCudaRenderTimestamped( } // CudaRaster - s.cr->setVertexBuffer(s.projectedVertices, actualVerts); - s.cr->setIndexBuffer(s.triangleIndices, actualTris); - s.cr->setTiebreakerColorBuffer(s.vertexColors); - s.cr->setDeterministicTiebreaker(true); - s.cr->deferredClear(0); + cr->setVertexBuffer(s.projectedVertices, actualVerts); + cr->setIndexBuffer(s.triangleIndices, actualTris); + cr->setTiebreakerColorBuffer(s.vertexColors); + cr->setDeterministicTiebreaker(true); + cr->deferredClear(0); int tilesX = (crWidth + maxVp - 1) / maxVp; int tilesY = (crHeight + maxVp - 1) / maxVp; @@ -3116,8 +3125,8 @@ void ludusCudaRenderTimestamped( for (int tx = 0; tx < tilesX; tx++) { int vpW = (tx < tilesX - 1) ? maxVp : (crWidth - tx * maxVp); int vpH = (ty < tilesY - 1) ? maxVp : (crHeight - ty * maxVp); - s.cr->setViewport(vpW, vpH, tx * maxVp, ty * maxVp); - if (!s.cr->drawTriangles(nullptr, false, stream)) { + cr->setViewport(vpW, vpH, tx * maxVp, ty * maxVp); + if (!cr->drawTriangles(nullptr, false, stream)) { fprintf(stderr, "[LudusCudaTS] cam %d tile (%d,%d): CudaRaster overflow; zeroing camera output\n", camIdx, tx, ty); @@ -3135,7 +3144,7 @@ void ludusCudaRenderTimestamped( continue; // Fragment pass (barycentric interpolation) - const uint32_t* crColor = (const uint32_t*)s.cr->getColorBuffer(); + const uint32_t* crColor = (const uint32_t*)cr->getColorBuffer(); if (smoothed) { dim3 fragGrid((ssW + 7) / 8, (ssH + 7) / 8); dim3 fragBlock(8, 8); diff --git a/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_cpp/render/ludus_cuda.h b/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_cpp/render/ludus_cuda.h index 21426b4a9..9aee9b79c 100644 --- a/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_cpp/render/ludus_cuda.h +++ b/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_cpp/render/ludus_cuda.h @@ -45,6 +45,7 @@ struct LudusCudaState { int width, height, numCameras; CR::CudaRaster* cr; + CR::CudaRaster* crBev; // Geometry buffers (GPU, managed by renderer) float* projectedVertices; // float4 per vertex diff --git a/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_ops/_plugin.py b/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_ops/_plugin.py index 8c5e4101b..e247ccfcc 100644 --- a/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_ops/_plugin.py +++ b/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/_ops/_plugin.py @@ -6,6 +6,7 @@ import glob import os import subprocess +import sys import time import torch @@ -83,7 +84,7 @@ def get_sort_key(x): if paths: return sorted(paths, key=get_sort_key)[-1] - if ( + if not getattr(sys, "frozen", False) and ( subprocess.run( ["where.exe", "cl.exe"], stdout=subprocess.DEVNULL, diff --git a/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/object_graph.py b/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/object_graph.py index f4318bd78..15f425c77 100644 --- a/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/object_graph.py +++ b/integrations_v2/omnidreams/impl/ludus-renderer/ludus_renderer/object_graph.py @@ -170,6 +170,9 @@ class PhysicsObjectGraph: _barrier_cells: dict[tuple[int, int], tuple[int, ...]] = field( init=False, default_factory=dict, repr=False, compare=False ) + _spatial_index_revision: int = field( + init=False, default=-1, repr=False, compare=False + ) def __post_init__(self) -> None: self.object_index = { @@ -178,7 +181,6 @@ def __post_init__(self) -> None: } if len(self.object_index) != len(self.objects): raise ValueError("object_id values must be unique") - self._rebuild_spatial_index() def _cells_for_bounds( self, minimum_xy: np.ndarray, maximum_xy: np.ndarray @@ -235,6 +237,7 @@ def _rebuild_spatial_index(self) -> None: self._barrier_cells = { cell: tuple(indices) for cell, indices in barrier_cells.items() } + self._spatial_index_revision = self.revision def _spatial_candidates( self, @@ -261,7 +264,6 @@ def upsert_object(self, scene_object: SceneObject) -> None: value.object_id: index for index, value in enumerate(self.objects) } self.revision += 1 - self._rebuild_spatial_index() def remove_object(self, object_id: str) -> None: """Remove one object without rebuilding unrelated graph data.""" @@ -276,7 +278,6 @@ def remove_object(self, object_id: str) -> None: value.object_id: index for index, value in enumerate(self.objects) } self.revision += 1 - self._rebuild_spatial_index() def upsert_barrier(self, barrier_id: str, barrier: InvisibleBarrier) -> None: """Add or replace one named invisible wall.""" @@ -296,7 +297,6 @@ def upsert_barrier(self, barrier_id: str, barrier: InvisibleBarrier) -> None: barriers.append(normalized) self.barriers = tuple(barriers) self.revision += 1 - self._rebuild_spatial_index() def remove_barrier(self, barrier_id: str) -> None: """Remove one named invisible wall.""" @@ -307,7 +307,6 @@ def remove_barrier(self, barrier_id: str) -> None: return self.barriers = barriers self.revision += 1 - self._rebuild_spatial_index() def copy_for_physx( self, @@ -328,6 +327,8 @@ def copy_for_physx( raise ValueError("center_xy_m must have shape (2,)") if radius_m <= 0.0: raise ValueError("radius_m must be positive") + if self._spatial_index_revision != self.revision: + self._rebuild_spatial_index() object_candidates = self._spatial_candidates( self._object_cells, center, float(radius_m) ) diff --git a/integrations_v2/omnidreams/impl/native/omnidreams_singleview.py b/integrations_v2/omnidreams/impl/native/omnidreams_singleview.py index 926d86463..04a8ba033 100644 --- a/integrations_v2/omnidreams/impl/native/omnidreams_singleview.py +++ b/integrations_v2/omnidreams/impl/native/omnidreams_singleview.py @@ -7,6 +7,7 @@ import contextlib import hashlib +import importlib.machinery import importlib.util import json import os @@ -551,6 +552,33 @@ def _scoped_cuda_arch_list(cuda_arch_list: str | None) -> Iterator[None]: os.environ[_PYTORCH_CUDA_ARCH_LIST_ENV] = previous +def _load_prebuilt_extension() -> ModuleType: + """Load the extension prepared in an offline bundle without build sources.""" + build_root = Path(os.environ["TORCH_EXTENSIONS_DIR"]) / "torch_extensions" + binaries = [ + (directory.name, binary) + for directory in build_root.glob("omnidreams_singleview_native_sage3_*") + for suffix in importlib.machinery.EXTENSION_SUFFIXES + if (binary := directory / f"{directory.name}{suffix}").is_file() + ] + # ponytail: One native variant per bundle; add an explicit manifest if bundles + # must support multiple precompiled CUDA architectures. + if len(binaries) != 1: + raise NativeSourcesUnavailable( + f"Expected one prebuilt OmniDreams extension in {build_root}, " + f"found {len(binaries)}." + ) + name, binary = binaries[0] + _add_windows_cuda_dll_directories(_python_package_dir("nvidia.cudnn")) + spec = importlib.util.spec_from_file_location(name, binary) + if spec is None or spec.loader is None: + raise ImportError(f"Cannot load prebuilt OmniDreams extension from {binary}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + sys.modules[name] = module + return module + + def load_extension( build_root: Path | str | None = None, *, @@ -578,6 +606,9 @@ def load_extension( _extension_load_error = None try: + if getattr(sys, "frozen", False): + _extension[extension_key] = _load_prebuilt_extension() + return _extension[extension_key] _ensure_windows_cuda13_toolkit() from torch.utils.cpp_extension import load as load_torch_extension diff --git a/integrations_v2/omnidreams/impl/omnidreams_singleview/tools/native_build.py b/integrations_v2/omnidreams/impl/omnidreams_singleview/tools/native_build.py index 6ea7a5e82..1fa0b4b3c 100644 --- a/integrations_v2/omnidreams/impl/omnidreams_singleview/tools/native_build.py +++ b/integrations_v2/omnidreams/impl/omnidreams_singleview/tools/native_build.py @@ -209,7 +209,9 @@ def validate_thirdparty() -> dict[str, SourceInfo]: def _default_build_root() -> Path: - override = os.environ.get(_DEFAULT_BUILD_ROOT_ENV) + override = os.environ.get(_DEFAULT_BUILD_ROOT_ENV) or os.environ.get( + "TORCH_EXTENSIONS_DIR" + ) if override: return Path(override).expanduser().resolve() return ROOT / "build" diff --git a/integrations_v2/omnidreams/impl/scripts/export_lightvae_fp8_state.py b/integrations_v2/omnidreams/impl/scripts/export_lightvae_fp8_state.py index 4630fd008..9b1c247fc 100644 --- a/integrations_v2/omnidreams/impl/scripts/export_lightvae_fp8_state.py +++ b/integrations_v2/omnidreams/impl/scripts/export_lightvae_fp8_state.py @@ -14,10 +14,11 @@ import torch from omnidreams.impl.vae_native import DEFAULT_LIGHTVAE_FP8_STATE_PATH +from flashdreams.core.io.disk import default_flashdreams_cache_dir from flashdreams.infra.config import derive_config DEFAULT_CONFIG = "omnidreams" -DEFAULT_STATE_PATH = Path(DEFAULT_LIGHTVAE_FP8_STATE_PATH) +DEFAULT_STATE_PATH = default_flashdreams_cache_dir() / DEFAULT_LIGHTVAE_FP8_STATE_PATH EXAMPLE_DATA_HF_REPO = "nvidia/omni-dreams-samples" DEFAULT_EXAMPLE_DATA_UUID = "239560dc-33d1-11ef-9720-00044bcbccac" VAE_FP8_VERSION_KEY = "__omnidreams_vae_fp8_version__" @@ -382,9 +383,13 @@ def export_lightvae_fp8_state( def ensure_lightvae_fp8_state( - out: Path = DEFAULT_STATE_PATH, + out: Path | None = None, ) -> Path: - target = out.expanduser().resolve() + target = ( + (out or default_flashdreams_cache_dir() / DEFAULT_LIGHTVAE_FP8_STATE_PATH) + .expanduser() + .resolve() + ) if target.is_file(): return target diff --git a/integrations_v2/omnidreams/impl/vae_native.py b/integrations_v2/omnidreams/impl/vae_native.py index c64d641a7..ac941a8cc 100644 --- a/integrations_v2/omnidreams/impl/vae_native.py +++ b/integrations_v2/omnidreams/impl/vae_native.py @@ -29,6 +29,7 @@ ) from torch import Tensor +from flashdreams.core.io.disk import default_flashdreams_cache_dir from flashdreams.recipes.wan.autoencoder.vae import ( TEMPORAL_WINDOW, WanVAECache, @@ -38,7 +39,7 @@ NativeVAEBackend = Literal["fp8"] NATIVE_LIGHTVAE_FP8_STATE_ENV = "OMNIDREAMS_LIGHTVAE_FP8_STATE_PATH" -DEFAULT_LIGHTVAE_FP8_STATE_PATH = "artifacts/native_vae/lightvae_fp8_state.pt" +DEFAULT_LIGHTVAE_FP8_STATE_PATH = "omnidreams/native_vae/lightvae_fp8_state.pt" @dataclass(kw_only=True) @@ -96,7 +97,13 @@ def _native_vae_fp8_state_path( ensure_lightvae_fp8_state, ) - return str(ensure_lightvae_fp8_state(Path(path or DEFAULT_LIGHTVAE_FP8_STATE_PATH))) + return str( + ensure_lightvae_fp8_state( + Path(path) + if path + else default_flashdreams_cache_dir() / DEFAULT_LIGHTVAE_FP8_STATE_PATH + ) + ) def _native_vae_availability_check( diff --git a/integrations_v2/omnidreams/tests/test_omnidreams_native_vae.py b/integrations_v2/omnidreams/tests/test_omnidreams_native_vae.py index 2fba0005b..5c94feb53 100644 --- a/integrations_v2/omnidreams/tests/test_omnidreams_native_vae.py +++ b/integrations_v2/omnidreams/tests/test_omnidreams_native_vae.py @@ -168,6 +168,7 @@ def test_fast_perf_config_auto_exports_default_fp8_state( from omnidreams.impl.scripts import export_lightvae_fp8_state as exporter monkeypatch.delenv(NATIVE_LIGHTVAE_FP8_STATE_ENV, raising=False) + monkeypatch.setenv("FLASHDREAMS_CACHE_DIR", str(tmp_path)) expected = tmp_path / DEFAULT_LIGHTVAE_FP8_STATE_PATH calls: list[Path] = [] @@ -185,8 +186,8 @@ def ensure(path: Path) -> Path: assert _native_vae_fp8_state_path(image_encoder) == str(expected) assert _native_vae_fp8_state_path(encoder) == str(expected) assert calls == [ - Path(DEFAULT_LIGHTVAE_FP8_STATE_PATH), - Path(DEFAULT_LIGHTVAE_FP8_STATE_PATH), + expected, + expected, ] diff --git a/integrations_v2/omnidreams/tests/test_recipe_configs.py b/integrations_v2/omnidreams/tests/test_recipe_configs.py index 2a9775f51..eb97d7f05 100644 --- a/integrations_v2/omnidreams/tests/test_recipe_configs.py +++ b/integrations_v2/omnidreams/tests/test_recipe_configs.py @@ -84,54 +84,6 @@ pytestmark = pytest.mark.ci_cpu -def test_options_export_includes_current_preset_comparisons(tmp_path: Path) -> None: - def unexpected(*args: object, **kwargs: object) -> object: - pytest.fail("Preset documentation export attempted model or scene preparation") - - app = CrazyRobotaxiApplication( - defaults=OMNIDREAMS_CRAZY_ROBOTAXI_DEFAULTS, - pipeline_factory=unexpected, - scene_factory=cast(Any, unexpected), - native_preparer=unexpected, - ) - output = tmp_path / "options.md" - with pytest.raises(SystemExit) as error: - app.init(["--export-options-docs", str(output)]) - - assert error.value.code == 0 - reference = output.read_text() - guide = reference.split("## OmniDreams runner presets\n", 1)[1] - project = Path(__file__).parents[1] / "pyproject.toml" - registry = tomllib.loads(project.read_text())["project"]["entry-points"][ - "flashdreams.applications_v2" - ] - overview = guide.split("### Standard and shared values", 1)[0] - runners = re.findall(r"^\| `(crazy-robotaxi[^`]+)` \|", overview, re.M) - assert set(runners) == { - slug for slug in registry if slug.startswith("crazy-robotaxi-") - } - assert len(runners) == 12 - assert "1280 × 704" in overview - assert "1024 × 560" in overview - - perf = guide.split("### `crazy-robotaxi-omnidreams-perf`\n", 1)[1].split("###", 1)[ - 0 - ] - assert "`[1000, 500]` | `[1000, 100]`" in perf - assert "`disabled` | `required`" in perf - fast = guide.split("### `crazy-robotaxi-omnidreams-fast-perf`\n", 1)[1].split( - "###", 1 - )[0] - assert "`model.pipeline.diffusion_model.seed`" in fast - assert "`42` | `null`" in fast - assert "`bfloat16` | `float16`" in fast - assert "`model.pipeline.image_encoder.native_vae_backend` | Preset-owned" in guide - assert "32 GB VRAM" in guide - assert "inactive" in guide - assert "schema_version: 1" in guide - assert app._config is None - - @pytest.mark.parametrize( "config", OMNIDREAMS_CONFIGS.values(), ids=OMNIDREAMS_CONFIGS.keys() ) diff --git a/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index 6946afa7c..31ba2bebf 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -1,136 +1 @@ -# Package a FlashDreams application for offline use - -`package_as_offline_exe.py` prepares an installed FlashDreams v2 application -and creates an isolated executable with all its dependencies in one folder. -Packaging is supported for applications running in `native-window` or `webrtc` -mode. - -## Usage - -From the repository root, use `:::` to separate this tool's options from the -complete `flashdreams-run-v2` command: - -```bash -uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ - --output artifacts/interactive-drive-omnidreams-bundle \ - ::: flashdreams-run-v2 interactive-drive-omnidreams \ - --mode native-window -``` - -The command after `:::` must begin with `flashdreams-run-v2`, and its effective -mode must be `native-window` or `webrtc`. - -To package the Interactive Drive browser experience, select `webrtc` normally: - -```bash -uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ - --output artifacts/interactive-drive-omnidreams-webrtc-bundle \ - ::: flashdreams-run-v2 interactive-drive-omnidreams \ - --mode webrtc --host 0.0.0.0 --port 8089 -``` - -Use the runtime command's `--` separator when the application needs its own -arguments: - -```bash -uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ - --output artifacts/interactive-drive-omnidreams-bundle \ - ::: flashdreams-run-v2 interactive-drive-omnidreams \ - --mode native-window \ - -- --game-mode -``` - -If `--output` is omitted, the default is -`artifacts/-bundle`. The output directory must not already -exist. - -## Runtime and application arguments - -Arguments before the runtime command's `--` are FlashDreams runtime arguments; -arguments after it are application arguments: - -```bash -uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ - --output artifacts/interactive-drive-omnidreams-bundle \ - ::: flashdreams-run-v2 interactive-drive-omnidreams \ - --mode native-window --timeout 120 \ - -- --game-mode -``` - -Do not pass `--preload-application`; the packager controls preparation so it -can validate and populate the bundle's caches. - -The generated launcher embeds the supplied application arguments to this installer program inside the executable. - -The packager runs initialization and one model block. Preparation findings are -written to `PREPARATION_ISSUES.txt` beside `INSTALLER_OUTPUT.txt`. - -## Options - -```text ---output PATH - Destination bundle directory. - ---preload-timeout SECONDS - Fail if initialization and preload do not finish within this time. - ---skip-runtime-validation - Skip the one-block runtime check. Initialization still runs. - ---hidden-import MODULE - Add a PyInstaller hidden import. Repeat for multiple modules. - ---collect-executable NAME - Include an executable found on PATH. Repeat for multiple executables. -``` - -For the authoritative command-line help, run: - -```bash -uv run python tools/package-as-offline-exe/package_as_offline_exe.py --help -``` - -## Generated bundle - -The generated directory has this shape: - -```text -/ -|-- [.exe] -|-- data/ -|-- cache/ -|-- INSTALLER_OUTPUT.txt -|-- PREPARATION_ISSUES.txt # only when preload warnings are found -`-- README.md -``` - -Notes on generated files: - -- `data/` contains the Python runtime, application code, dependencies, native - libraries, and packaged GPU assets. -- `cache/` is the offline seed containing resources and build artifacts - prepared during initialization. -- `INSTALLER_OUTPUT.txt` contains the application preload/validation and - PyInstaller build output, with the exit code for each completed step. -- `PREPARATION_ISSUES.txt` contains preload warnings and their application call - stacks when validation finds preparation work outside `IApplication.init`. -- The generated `README.md` explains how to launch a generated bundle. -- `[.exe]` is the executable that can be launched to run the application. - - Windows builds use the `.exe` suffix; Linux builds do not. - -## Runtime cache behavior - -The packaged `cache/` directory remains read-only. On first launch of the executable, -the launcher copies it into a writable per-user cache: - -- Windows: `%LOCALAPPDATA%\FlashDreams\\cache` -- Linux: `$XDG_CACHE_HOME/flashdreams/`, or - `~/.cache/flashdreams/` when `XDG_CACHE_HOME` is unset - -Set `FLASHDREAMS_RUNTIME_CACHE_DIR` if a custom cache location is desired. - -Do not move or distribute only the executable. It depends on the sibling -`data/` and `cache/` directories. Set `FLASHDREAMS_RUNTIME_CACHE_DIR` to move -the writable cache. - -Destination machine still needs a compatible NVIDIA driver to run the packaged application. +[Refer To Website For Documentation](https://flashdreams.nvidia.com/documentation/tools/offline_program_packager.html) diff --git a/tools/package-as-offline-exe/check_launcher_streams.py b/tools/package-as-offline-exe/check_launcher_streams.py new file mode 100644 index 000000000..2562a7fd9 --- /dev/null +++ b/tools/package-as-offline-exe/check_launcher_streams.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import io +import os +import runpy +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace +from typing import Any +from unittest.mock import patch + +from loguru import logger + +packager = runpy.run_path(str(Path(__file__).with_name("package_as_offline_exe.py"))) +source = packager["_launcher_source"]("demo", []) +compile(source, "launcher.py", "exec") +startup = source.split("\nfrom flashdreams.runtime_v2.cli import", 1)[0] + +for missing_stdout, missing_stderr in ( + (True, True), + (True, False), + (False, True), + (False, False), +): + stdout = None if missing_stdout else io.StringIO() + stderr = None if missing_stderr else io.StringIO() + with ( + TemporaryDirectory() as directory, + patch("tempfile.gettempdir", return_value=directory), + patch.dict(os.environ), + patch.object(sys, "argv", ["launcher.py"]), + patch.object(sys, "stdout", stdout), + patch.object(sys, "stderr", stderr), + ): + namespace: dict[str, Any] = {"__file__": str(Path(directory) / "launcher.py")} + exec(startup, namespace) + assert sys.stdout is not None and sys.stderr is not None + assert missing_stdout or sys.stdout is stdout + assert missing_stderr or sys.stderr is stderr + handler = logger.add(sys.stderr, format="{message}") + try: + logger.info("launcher logging works") + sys.stdout.write("launcher output works\n") + if missing_stdout or missing_stderr: + log = Path(directory) / "flashdreams" / "demo" / "runtime.log" + contents = log.read_text(encoding="utf-8") + assert not missing_stderr or "launcher logging works" in contents + assert not missing_stdout or "launcher output works" in contents + else: + assert "runtime_log" not in namespace + finally: + logger.remove(handler) + if "runtime_log" in namespace: + namespace["runtime_log"].close() + +with TemporaryDirectory() as directory: + output = Path(directory).resolve() / "bundle" + + def build_step(command, **options): + staging = Path(options["cwd"]) + if command == ["build"]: + runtime = staging / ".build" / "dist" / "demo" + (runtime / "_internal").mkdir(parents=True) + (runtime / packager["_executable_name"]("demo")).touch() + (runtime / "_internal" / "python.dll").write_bytes(b"runtime") + else: + (staging / "cache" / "weights.bin").write_bytes(b"weights") + return SimpleNamespace(returncode=0) + + with ( + patch.dict( + packager["package"].__globals__, + { + "_application_module": lambda slug: "demo", + "_cache_environment": lambda *args, **kwargs: os.environ.copy(), + "_pyinstaller_command": lambda **kwargs: ["build"], + }, + ), + patch("subprocess.run", side_effect=build_step), + patch("importlib.util.find_spec", return_value=SimpleNamespace()), + ): + packager["package"]("demo", ["--mode", "native-window"], output) + assert (output / "_internal" / "python.dll").read_bytes() == b"runtime" + assert (output / "cache" / "weights.bin").read_bytes() == b"weights" + +print("Launcher stream and bundle layout checks passed.") diff --git a/tools/package-as-offline-exe/package_as_offline_exe.py b/tools/package-as-offline-exe/package_as_offline_exe.py index ca630800e..d26071b89 100644 --- a/tools/package-as-offline-exe/package_as_offline_exe.py +++ b/tools/package-as-offline-exe/package_as_offline_exe.py @@ -34,9 +34,6 @@ _COMMAND_SEPARATOR = ":::" """Separator between this tool's options and a complete runtime command.""" -_FAILURE_GUIDANCE = "Packager failed. This is either a packager bug, missing environment variable, or packaged application crash." -"""Guidance appended to application-preload failures.""" - _SAFE_SLUG = re.compile(r"^[A-Za-z0-9_.-]+$") _INSTALLER_OUTPUT = "INSTALLER_OUTPUT.txt" _PREPARATION_ISSUES = "PREPARATION_ISSUES.txt" @@ -90,6 +87,16 @@ def _parser() -> argparse.ArgumentParser: type=Path, help="Bundle directory (default: artifacts/-bundle).", ) + parser.add_argument( + "--working-dir", + type=Path, + help="Directory contents to copy before preload (default: output directory).", + ) + parser.add_argument( + "--noconsole", + action="store_true", + help="Build without a console window on Windows (ignored on Linux).", + ) parser.add_argument( "--preload-timeout", type=float, @@ -181,6 +188,13 @@ def _cache_environment( ) -> dict[str, str]: """Return an environment with every supported cache below ``cache_root``.""" env = os.environ.copy() + env["PATH"] = os.pathsep.join( + ( + str(_ninja_executable().parent), + str(Path(sys.executable).parent), + env.get("PATH", ""), + ) + ) env["HF_HUB_DISABLE_SYMLINKS"] = "1" for name, relative_path in _CACHE_PATHS.items(): cache_path = cache_root / relative_path @@ -191,6 +205,13 @@ def _cache_environment( return env +def _log_tail(path: Path) -> str: + """Return the last lines of a failed subprocess log.""" + return "\n".join( + path.read_text(encoding="utf-8", errors="replace").splitlines()[-30:] + ) + + def _application_module(slug: str) -> str: """Return the module registered for ``slug`` or its import fallback.""" for entry_point in entry_points(group=_APPLICATION_ENTRY_POINT_GROUP): @@ -384,48 +405,78 @@ def _launcher_source( f"""\ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 + import argparse import importlib.machinery import importlib.util import os import shutil import sys from pathlib import Path + from tempfile import gettempdir + if sys.stdout is None or sys.stderr is None: + log_root = Path(gettempdir()) / "flashdreams" / {slug!r} + log_root.mkdir(parents=True, exist_ok=True) + runtime_log = (log_root / "runtime.log").open( + "a", encoding="utf-8", buffering=1 + ) + if sys.stdout is None: + sys.stdout = runtime_log + if sys.stderr is None: + sys.stderr = runtime_log + + def filesystem_path(path): + if os.name != "nt": + return path + prefix = os.sep * 2 + "?" + os.sep + absolute = str(path) + if absolute.startswith(prefix): + return path + if absolute.startswith(os.sep * 2): + absolute = "UNC" + os.sep + absolute[2:] + return Path(prefix + absolute) + + parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False) + parser.add_argument("--unpack-dir") + unpack_options, remaining_arguments = parser.parse_known_args(sys.argv[1:]) bundle_root = ( Path(sys.executable).resolve().parent if getattr(sys, "frozen", False) else Path(__file__).resolve().parent ) + if getattr(sys, "frozen", False): + os.chdir(bundle_root) seed_cache_root = bundle_root / "cache" - seed_cache_root.mkdir(parents=True, exist_ok=True) cache_root = seed_cache_root cache_seed_id = {cache_seed_id!r} if getattr(sys, "frozen", False): + unpack_dir = unpack_options.unpack_dir configured_cache_root = os.environ.get({_RUNTIME_CACHE_ROOT_ENV!r}) - if configured_cache_root: + if unpack_dir: + unpack_root = Path(unpack_dir).expanduser() + if not unpack_root.is_absolute(): + raise RuntimeError( + "--unpack-dir must be an absolute path." + ) + cache_root = unpack_root / {slug!r} / "cache" + elif configured_cache_root: cache_root = Path(configured_cache_root).expanduser() if not cache_root.is_absolute(): raise RuntimeError( f"{_RUNTIME_CACHE_ROOT_ENV} must be an absolute path." ) - elif os.name == "nt": - local_app_data = os.environ.get("LOCALAPPDATA") - if not local_app_data: + else: + profile = os.environ.get("USERPROFILE") if os.name == "nt" else None + if os.name == "nt" and not profile: raise RuntimeError( - "LOCALAPPDATA is required unless " - f"{_RUNTIME_CACHE_ROOT_ENV} is set." + "USERPROFILE is required unless --unpack-dir is set." ) cache_root = ( - Path(local_app_data) - / "FlashDreams" - / {slug!r} - / "cache" - ) - else: - cache_root = ( - Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) + Path(profile or Path.home()) / "flashdreams" + / "unpacked" / {slug!r} + / "cache" ) cache_root = cache_root.resolve() @@ -438,6 +489,8 @@ def _launcher_source( "The runtime cache cannot contain, or be contained by, " "the bundled cache." ) + cache_root = filesystem_path(cache_root) + seed_cache_root = filesystem_path(seed_cache_root) marker = cache_root / {_RUNTIME_CACHE_SEED_MARKER!r} if cache_seed_id is not None and ( not marker.is_file() @@ -454,6 +507,13 @@ def _launcher_source( os.environ[name] = str(cache_path) if getattr(sys, "frozen", False): + # Prepared native code must load from the deployed bundle. + for name in ( + "TORCH_EXTENSIONS_DIR", + "TRITON_CACHE_DIR", + "FLASHDREAMS_TRITON_CACHE_DIR", + ): + os.environ[name] = str(seed_cache_root / cache_paths[name]) runtime_root = Path(getattr(sys, "_MEIPASS", bundle_root)) os.environ["PATH"] = ( str(runtime_root) + os.pathsep + os.environ.get("PATH", "") @@ -469,8 +529,6 @@ def _launcher_source( # those binaries because rebuilding would require a target-side compiler. from torch.utils import cpp_extension - original_cpp_extension_load = cpp_extension.load - def load_cpp_extension(name, *args, **kwargs): extension_dir = Path(os.environ["TORCH_EXTENSIONS_DIR"]) / name if kwargs.get("is_python_module", True): @@ -485,7 +543,10 @@ def load_cpp_extension(name, *args, **kwargs): spec.loader.exec_module(module) sys.modules[name] = module return module - return original_cpp_extension_load(name, *args, **kwargs) + raise RuntimeError( + f"Preloaded native extension {{name!r}} is missing from " + f"{{extension_dir}}; rebuild the offline bundle." + ) cpp_extension.load = load_cpp_extension @@ -496,7 +557,9 @@ def load_cpp_extension(name, *args, **kwargs): else: def reuse_preloaded_physx(cache_root, _physx_root): module_dir = ( - Path(cache_root) + seed_cache_root + / "ludus" + / "physx" / "module" / (sys.implementation.cache_tag or "python") ) @@ -512,7 +575,7 @@ def reuse_preloaded_physx(cache_root, _physx_root): from flashdreams.runtime_v2.cli import entrypoint, split_arguments - runtime_arguments, application_arguments = split_arguments(sys.argv[1:]) + runtime_arguments, application_arguments = split_arguments(remaining_arguments) application_arguments = {embedded_application_arguments!r} + application_arguments entrypoint([ {slug!r}, @@ -571,32 +634,44 @@ def _bundle_readme_source(slug: str) -> str: The application and both runtime/application argument sets are embedded in the executable. Extra command-line arguments are appended to that configuration. - The packaged `cache/` is copied once to a writable per-user cache. Set - `{_RUNTIME_CACHE_ROOT_ENV}` to an absolute writable path to override it. - The packaged cache remains the offline seed. + The packaged `cache/` is copied into + `%USERPROFILE%\\flashdreams\\unpacked\\{slug}\\cache` on Windows. + Pass `--unpack-dir C:\\path\\to\\directory` to put it under + `C:\\path\\to\\directory\\{slug}\\cache` instead. The unpack + directory must be absolute. `{_RUNTIME_CACHE_ROOT_ENV}` still sets + the exact cache directory when `--unpack-dir` is absent. + + Without a console, stdout and stderr append to + `/flashdreams/{slug}/runtime.log`, where `` is the system + temporary directory (`%TEMP%` on Windows). ## Expected processes during runtime | Process | Expected count | Purpose | | --- | ---: | --- | - | `{executable}` | 1 | Hosts the model loop and selected client-window UI. | + | `{executable}` | 1 | Hosts the model loop and client-window UI. | `python`, `cmake`, and `ninja` processes are not expected during normal runtime; Python and validated native extensions are contained in this bundle. + If a prebuilt extension is missing, rebuild the bundle on the packaging host. ## Bundle root contents | Path | Description | | --- | --- | - | `{executable}` | Compiled launcher for this operating system. | - | `data/` | Python runtime, application code, native libraries, and GPU assets. | + | `{executable}` | Launcher and embedded Python bytecode for this operating system. | + | `_internal/` | Python runtime, native libraries, supporting source files, and GPU assets. | | `cache/` | Model weights, scenes, compiled kernels, and native extensions. | + | `artifacts/` | Optional application assets prepared during preload. | | `{_INSTALLER_OUTPUT}` | Application preload/validation and PyInstaller output. | | `{_PREPARATION_ISSUES}` | Optional preload warnings with application call stacks. | | `README.md` | This launch and bundle-layout guide. | - Keep the complete directory together. The executable is not standalone from - `data/` and `cache/`, and moving only the executable will not work. + Deploy the complete raw folder, including `_internal/`, `cache/`, and any + `artifacts/` assets. Runtime files are loaded from the deployed folder; + prepared C++, Triton, and PhysX extensions load from the bundled `cache/`. + Embedded Python bytecode and the standard-library archive are read directly, + without extraction. Runtime files are not extracted at launch. """ ) @@ -609,10 +684,16 @@ def _pyinstaller_command( build_root: Path, hidden_imports: tuple[str, ...] = (), executables: tuple[str, ...] = (), + noconsole: bool = False, ) -> list[str]: """Prepare and return the PyInstaller command for an application package.""" module_root = application_module.partition(".")[0] slangpy_shaders = _module_search_path("slangpy") / "slangpy" / "shaders" + imgui_fonts = ( + _module_search_path("imgui_bundle") / "imgui_bundle" / "assets" / "fonts" + ) + if not (imgui_fonts / "DroidSans.ttf").is_file(): + raise PackageError(f"Cannot locate imgui font {imgui_fonts / 'DroidSans.ttf'}.") metadata_distributions = _metadata_distributions(application_module) local_modules = {module_root} local_modules.update(_local_dependency_modules(metadata_distributions)) @@ -638,8 +719,6 @@ def _pyinstaller_command( "--onedir", "--name", slug, - "--contents-directory", - "data", "--distpath", str(build_root / "dist"), "--workpath", @@ -648,6 +727,9 @@ def _pyinstaller_command( str(build_root / "spec"), "--additional-hooks-dir", str(hooks), + # IPython treats Jedi as optional; its hook otherwise collects thousands of stubs. + "--exclude-module", + "jedi", "--paths", str(_module_search_path("flashdreams.runtime_v2")), "--collect-data", @@ -660,11 +742,15 @@ def _pyinstaller_command( "slangpy", "--add-data", f"{slangpy_shaders}{os.pathsep}shaders", + "--add-data", + f"{imgui_fonts}{os.pathsep}imgui_bundle/assets/fonts", "--collect-submodules", "transformers", "--hidden-import", application_module, ] + if noconsole: + command.append("--noconsole") for module in sorted(local_modules): command.extend(("--paths", str(_module_search_path(module)))) command.extend(("--collect-all", module)) @@ -680,6 +766,7 @@ def _pyinstaller_command( for binary, installed_parent in _nvrtc_builtins(): for destination in sorted({Path("."), installed_parent}): command.extend(("--add-binary", f"{binary}{os.pathsep}{destination}")) + command.extend(("--add-binary", f"{_ninja_executable()}{os.pathsep}.")) for executable_name in executables: executable = shutil.which(executable_name) if executable is None: @@ -689,6 +776,19 @@ def _pyinstaller_command( return command +def _ninja_executable() -> Path: + """Find Ninja in the active Python environment or on PATH.""" + try: + import ninja + except ImportError: + executable = shutil.which("ninja") + else: + executable = shutil.which("ninja", path=ninja.BIN_DIR) or shutil.which("ninja") + if executable is None: + raise PackageError("Ninja is required to package PyTorch C++ extensions.") + return Path(executable).resolve() + + def _validate_slug(slug: str) -> None: """Reject slugs that cannot safely become directory or executable names.""" if not _SAFE_SLUG.fullmatch(slug) or slug in {".", ".."}: @@ -707,10 +807,12 @@ def package( slug: str, config: list[str], output: Path, + working_dir: Path | None = None, preload_timeout: float | None = None, hidden_imports: tuple[str, ...] = (), executables: tuple[str, ...] = (), skip_runtime_validation: bool = False, + noconsole: bool = False, ) -> Path: """Preload and freeze one configured v2 application into ``output``. @@ -718,10 +820,12 @@ def package( slug: Registered v2 application slug. config: Arguments after the slug in ``flashdreams-run-v2``. output: Final bundle directory, which must not already exist. + working_dir: Directory contents to seed the bundle; defaults to ``output``. preload_timeout: Optional limit for application preload and validation. hidden_imports: Additional modules that PyInstaller cannot discover. executables: Executables from PATH required by the frozen runtime. skip_runtime_validation: Skip the preload's one-block runtime check. + noconsole: Build without a console window on Windows. Returns: Absolute path to the completed bundle. @@ -746,7 +850,29 @@ def package( if destination.exists(): raise PackageError(f"Output already exists: {destination}") destination.parent.mkdir(parents=True, exist_ok=True) - staging = destination.with_name(f".fd-{uuid.uuid4().hex}.tmp") + staging = destination + working_dir = (working_dir or destination).expanduser().resolve() + if working_dir != destination: + if destination.is_relative_to(working_dir) or working_dir.is_relative_to( + destination + ): + raise PackageError( + "Separate working and output directories must not overlap." + ) + working_dir.mkdir(parents=True, exist_ok=True) + reserved = { + "cache", + "_internal", + ".build", + "launcher.py", + "preload.py", + "README.md", + _INSTALLER_OUTPUT, + _PREPARATION_ISSUES, + _executable_name(slug), + } + if any((working_dir / name).exists() for name in reserved): + raise PackageError("Working directory contents conflict with bundle files.") staging.mkdir() launcher = staging / "launcher.py" preload = staging / "preload.py" @@ -757,6 +883,13 @@ def package( succeeded = False try: + if working_dir != destination: + try: + shutil.copytree(working_dir, staging, dirs_exist_ok=True) + except OSError as error: + raise PackageError( + f"Could not copy working directory: {error}" + ) from error cache_root.mkdir() preload.write_text( _preload_source( @@ -773,6 +906,7 @@ def package( output_stream.flush() preload_result = subprocess.run( [sys.executable, str(preload)], + cwd=staging, env=_cache_environment( cache_root, preparation_issues_path=preparation_issues, @@ -789,12 +923,12 @@ def package( with installer_output.open("ab") as output_stream: output_stream.write(b"\nTimed out.\n\n") raise PackageError( - f"Preload timed out for {slug!r}.\n{_FAILURE_GUIDANCE}" + f"Preload timed out for {slug!r}.\n{_log_tail(installer_output)}" ) from error if preload_result.returncode: raise PackageError( f"Preload failed for {slug!r} with exit code " - f"{preload_result.returncode}.\n{_FAILURE_GUIDANCE}" + f"{preload_result.returncode}.\n{_log_tail(installer_output)}" ) application_module = _application_module(slug) @@ -814,6 +948,7 @@ def package( build_root=build_root, hidden_imports=hidden_imports, executables=executables, + noconsole=noconsole, ), cwd=staging, env=_cache_environment(cache_root), @@ -826,18 +961,16 @@ def package( ) if pyinstaller_result.returncode: raise PackageError( - f"PyInstaller failed with exit code {pyinstaller_result.returncode}." - ) - - built_bundle = build_root / "dist" / slug - executable = built_bundle / executable_name - if not executable.is_file() or not (built_bundle / "data").is_dir(): - raise PackageError( - "PyInstaller completed without the expected executable/data layout." + f"PyInstaller failed with exit code {pyinstaller_result.returncode}.\n" + f"{_log_tail(installer_output)}" ) - for item in built_bundle.iterdir(): - shutil.move(str(item), staging / item.name) + runtime_bundle = build_root / "dist" / slug + executable = runtime_bundle / executable_name + if not executable.is_file(): + raise PackageError("PyInstaller completed without the expected executable.") + for path in runtime_bundle.iterdir(): + shutil.move(str(path), staging / path.name) (staging / "README.md").write_text( _bundle_readme_source(slug), encoding="utf-8" ) @@ -845,7 +978,6 @@ def package( shutil.rmtree(build_root) launcher.unlink() preload.unlink() - staging.replace(destination) succeeded = True return destination finally: @@ -869,6 +1001,8 @@ def main(arguments: list[str] | None = None) -> int: hidden_imports=tuple(parsed.hidden_import), executables=tuple(parsed.collect_executable), skip_runtime_validation=parsed.skip_runtime_validation, + noconsole=parsed.noconsole, + working_dir=parsed.working_dir, ) except PackageError as error: print(f"ERROR: {error}", file=sys.stderr) diff --git a/tools/package-as-offline-exe/test_package_as_offline_exe.py b/tools/package-as-offline-exe/test_package_as_offline_exe.py deleted file mode 100644 index 95cf33662..000000000 --- a/tools/package-as-offline-exe/test_package_as_offline_exe.py +++ /dev/null @@ -1,184 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""CPU tests for the offline packager command.""" - -import os -import runpy -import sys -from pathlib import Path -from typing import Any - -import pytest - -from flashdreams.runtime_v2 import cli - -pytestmark = pytest.mark.ci_cpu - -_PACKAGER: dict[str, Any] = runpy.run_path( - str(Path(__file__).with_name("package_as_offline_exe.py")) -) - - -@pytest.mark.parametrize( - "config", - (["--mode", "native-window"], ["--mode=webrtc"]), -) -def test_interactive_modes_can_be_packaged(config: list[str]) -> None: - _PACKAGER["_validate_config"](config) - - -def test_noninteractive_mode_cannot_be_packaged() -> None: - with pytest.raises( - _PACKAGER["PackageError"], - match="Packaging requires --mode native-window or webrtc", - ): - _PACKAGER["_validate_config"](["--mode", "mp4"]) - - -def test_slangpy_shaders_are_bundled_beside_runtime_library( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - packager_globals = _PACKAGER["_pyinstaller_command"].__globals__ - monkeypatch.setitem( - packager_globals, "_module_search_path", lambda _module: tmp_path - ) - monkeypatch.setitem(packager_globals, "_metadata_distributions", lambda _module: ()) - monkeypatch.setitem( - packager_globals, "_local_dependency_modules", lambda _distributions: () - ) - monkeypatch.setitem(packager_globals, "_nvrtc_builtins", lambda: ()) - - command = _PACKAGER["_pyinstaller_command"]( - launcher=tmp_path / "launcher.py", - slug="example", - application_module="example", - build_root=tmp_path / "build", - ) - - add_data = command.index("--add-data") - assert command[add_data + 1] == ( - f"{tmp_path / 'slangpy' / 'shaders'}{os.pathsep}shaders" - ) - - -def test_nvrtc_builtins_are_bundled_beside_nvrtc( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - packager_globals = _PACKAGER["_pyinstaller_command"].__globals__ - builtins = tmp_path / "nvidia" / "cu13" / "lib" / "libnvrtc-builtins.so.13.0" - monkeypatch.setitem( - packager_globals, "_module_search_path", lambda _module: tmp_path - ) - monkeypatch.setitem(packager_globals, "_metadata_distributions", lambda _module: ()) - monkeypatch.setitem( - packager_globals, "_local_dependency_modules", lambda _distributions: () - ) - monkeypatch.setitem( - packager_globals, - "_nvrtc_builtins", - lambda: ((builtins, Path("nvidia/cu13/lib")),), - ) - - command = _PACKAGER["_pyinstaller_command"]( - launcher=tmp_path / "launcher.py", - slug="example", - application_module="example", - build_root=tmp_path / "build", - ) - - binaries = [ - command[index + 1] - for index, argument in enumerate(command) - if argument == "--add-binary" - ] - assert f"{builtins}{os.pathsep}." in binaries - assert f"{builtins}{os.pathsep}{Path('nvidia/cu13/lib')}" in binaries - - -def test_skip_runtime_validation_keeps_application_preload() -> None: - parsed, command = _PACKAGER["_parse_arguments"]( - [ - "--skip-runtime-validation", - ":::", - "flashdreams-run-v2", - "example", - "--mode", - "native-window", - ] - ) - - assert parsed.skip_runtime_validation - source = _PACKAGER["_preload_source"]( - "example", command[2:], skip_runtime_validation=True - ) - assert "--preload-application" in source - assert "--skip-preload-validation" in source - - -def test_runtime_validation_uses_no_window_mode() -> None: - source = _PACKAGER["_preload_source"]( - "example", - [ - "--mode", - "native-window", - "--pixel-width", - "64", - "--", - "--model-option", - ], - ) - - assert ( - "entrypoint(['example', '--preload-application', '--pixel-width', '64', " - "'--', '--model-option'])" in source - ) - - -def test_launcher_keeps_runtime_and_application_overrides_separate( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - launched: list[list[str]] = [] - monkeypatch.setattr(cli, "entrypoint", lambda arguments: launched.append(arguments)) - monkeypatch.setattr( - sys, - "argv", - ["example", "--timeout", "5", "--", "--launch-option"], - ) - for name in _PACKAGER["_CACHE_PATHS"]: - monkeypatch.setenv(name, "") - - source = _PACKAGER["_launcher_source"]( - "example", - ["--mode", "native-window", "--", "--embedded-option"], - ) - launcher = tmp_path / "launcher.py" - exec(compile(source, launcher, "exec"), {"__file__": str(launcher)}) - - assert launched == [ - [ - "example", - "--mode", - "native-window", - "--timeout", - "5", - "--", - "--embedded-option", - "--launch-option", - ] - ] - - -def test_preparation_issues_are_written_beside_installer_output( - tmp_path: Path, -) -> None: - staging = tmp_path / "bundle" - issues_path = staging / "PREPARATION_ISSUES.txt" - - environment = _PACKAGER["_cache_environment"]( - staging / "cache", - preparation_issues_path=issues_path, - ) - - assert environment["FLASHDREAMS_PREPARATION_ISSUES_PATH"] == str(issues_path) - assert issues_path.parent == staging