From dce0eb0f5c4d5e9d6872b407f6af1cbf41257757 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Wed, 7 Oct 2026 15:47:57 -0700 Subject: [PATCH 01/16] condense demo packages for file-count reasons --- apps/crazy_robotaxi/README.md | 2 +- .../crazy_robotaxi/live_edit/config.py | 6 +- .../crazy_robotaxi/tests/test_live_edit_v2.py | 15 ++ .../impl/native/omnidreams_singleview.py | 32 ++++ .../tools/native_build.py | 4 +- .../impl/scripts/export_lightvae_fp8_state.py | 11 +- integrations_v2/omnidreams/impl/vae_native.py | 11 +- .../tests/test_omnidreams_native_vae.py | 5 +- tools/package-as-offline-exe/README.md | 51 ++++-- .../package_as_offline_exe.py | 153 +++++++++++++++--- .../test_package_as_offline_exe.py | 137 ++++++++++++++++ 11 files changed, 382 insertions(+), 45 deletions(-) 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/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/integrations_v2/omnidreams/impl/native/omnidreams_singleview.py b/integrations_v2/omnidreams/impl/native/omnidreams_singleview.py index 926d86463..537b0b872 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,34 @@ def _scoped_cuda_arch_list(cuda_arch_list: str | None) -> Iterator[None]: os.environ[_PYTORCH_CUDA_ARCH_LIST_ENV] = previous +def _load_prebuilt_extension(sage3_disabled: bool) -> ModuleType: + """Load the extension prepared in an offline bundle without build sources.""" + build_root = Path(os.environ["TORCH_EXTENSIONS_DIR"]) / "torch_extensions" + prefix = f"omnidreams_singleview_native_sage3_{int(not sage3_disabled)}_" + binaries = [ + (directory.name, binary) + for directory in build_root.glob(f"{prefix}*") + 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 +607,9 @@ def load_extension( _extension_load_error = None try: + if getattr(sys, "frozen", False): + _extension[extension_key] = _load_prebuilt_extension(sage3_disabled) + 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/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index 6946afa7c..ccb778032 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -1,7 +1,7 @@ # 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. +and creates a single executable with its offline cache in one folder. Packaging is supported for applications running in `native-window` or `webrtc` mode. @@ -13,8 +13,8 @@ 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 + ::: flashdreams-run-v2 interactive-drive-omnidreams-fast-perf \ + --mode native-window -- --game-mode ``` The command after `:::` must begin with `flashdreams-run-v2`, and its effective @@ -44,6 +44,32 @@ If `--output` is omitted, the default is `artifacts/-bundle`. The output directory must not already exist. +To package the fast-perf Interactive Drive game mode: + +```powershell +uv run python tools/package-as-offline-exe/package_as_offline_exe.py ` + --output artifacts/interactive-drive-omnidreams-fast-perf-onefile-1700 ` + ::: flashdreams-run-v2 interactive-drive-omnidreams-fast-perf ` + --mode native-window -- --game-mode +``` + +The output is still a directory. `cache/` holds the downloaded model weights +and prepared kernels. When a onefile bundle would exceed 1,700 files, the +packager stores the largest cache directories in `cache-extras.zip`. The +launcher extracts that archive into the writable runtime cache on first launch; +subsequent launches reuse it. Preload may also produce sibling `artifacts/` +assets. Set `FLASHDREAMS_RUNTIME_CACHE_DIR` to a path outside the bundle if +overriding the default per-user cache. +PyInstaller extracts the embedded runtime files to a temporary directory on +each launch, which adds startup time. + +On a Windows build of `interactive-drive-omnidreams-fast-perf` on 2026-10-07, +the completed bundle contained **98 files**: a 2.28 GiB EXE, a 0.47 GiB cache +archive, and 93 files in the 21.79 GiB `cache/`. A one-step native-window +game-mode run used the default per-user cache, restored the PhysX tree, rendered +five frames, and exited successfully in 136.1 seconds. This timing is +host-specific and includes model startup. + ## Runtime and application arguments Arguments before the runtime command's `--` are FlashDreams runtime arguments; @@ -97,17 +123,21 @@ The generated directory has this shape: ```text / |-- [.exe] -|-- data/ |-- cache/ +|-- cache-extras.zip # when needed to meet the file limit +|-- artifacts/ # when preload produces application assets |-- INSTALLER_OUTPUT.txt |-- PREPARATION_ISSUES.txt # only when preload warnings are found `-- README.md ``` +The executable contains the Python runtime, application code, dependencies, +native libraries, Ninja, and packaged GPU assets. The bundle has at most +1,700 files; `cache-extras.zip` holds cache files moved out of the directory +tree to meet that limit. + 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 @@ -118,6 +148,9 @@ Notes on generated files: - `[.exe]` is the executable that can be launched to run the application. - Windows builds use the `.exe` suffix; Linux builds do not. +The launcher sets its working directory to the bundle directory when started, +so relative application paths resolve beside the executable. + ## Runtime cache behavior The packaged `cache/` directory remains read-only. On first launch of the executable, @@ -129,8 +162,8 @@ the launcher copies it into a writable per-user cache: 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. +Keep the executable with its sibling `cache/`, any `cache-extras.zip`, and any +`artifacts/` assets. Set `FLASHDREAMS_RUNTIME_CACHE_DIR` to move the writable +cache. Destination machine still needs a compatible NVIDIA driver to run the packaged application. 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..501b3a6e5 100644 --- a/tools/package-as-offline-exe/package_as_offline_exe.py +++ b/tools/package-as-offline-exe/package_as_offline_exe.py @@ -15,6 +15,7 @@ import subprocess import sys import uuid +import zipfile from importlib.metadata import ( PackageNotFoundError, distribution, @@ -34,14 +35,13 @@ _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" _RUNTIME_CACHE_ROOT_ENV = "FLASHDREAMS_RUNTIME_CACHE_DIR" _RUNTIME_CACHE_SEED_MARKER = ".flashdreams-cache-seed" +_CACHE_ARCHIVE = "cache-extras.zip" +_ONEFILE_MAX_FILES = 1700 _SUPPORTED_MODES = ("native-window", "webrtc") _CACHE_PATHS = { @@ -181,6 +181,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 +198,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): @@ -389,6 +403,7 @@ def _launcher_source( import os import shutil import sys + import zipfile from pathlib import Path bundle_root = ( @@ -396,6 +411,8 @@ def _launcher_source( 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 @@ -430,6 +447,12 @@ def _launcher_source( cache_root = cache_root.resolve() seed_cache_root = seed_cache_root.resolve() + cache_archive = bundle_root / {_CACHE_ARCHIVE!r} + if cache_archive.is_file() and cache_root == seed_cache_root: + raise RuntimeError( + f"{_RUNTIME_CACHE_ROOT_ENV} must point outside the bundle " + "when cache-extras.zip is present." + ) if cache_root != seed_cache_root: if cache_root.is_relative_to( seed_cache_root @@ -444,6 +467,13 @@ def _launcher_source( or marker.read_text(encoding="utf-8") != cache_seed_id ): shutil.copytree(seed_cache_root, cache_root, dirs_exist_ok=True) + if cache_archive.is_file(): + with zipfile.ZipFile(cache_archive) as archive: + for member in archive.namelist(): + relative = Path(member) + if relative.is_absolute() or ".." in relative.parts: + raise RuntimeError(f"Unsafe cache archive path: {{member}}") + archive.extractall(cache_root) marker.write_text(cache_seed_id, encoding="utf-8") cache_paths = {_CACHE_PATHS!r} @@ -579,24 +609,28 @@ def _bundle_readme_source(slug: str) -> str: | Process | Expected count | Purpose | | --- | ---: | --- | - | `{executable}` | 1 | Hosts the model loop and selected client-window UI. | + | `{executable}` | 2 | One process extracts and later cleans up the runtime; the other 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. + Python and validated native extensions are contained in this bundle. Ninja + is included if a native extension needs to rebuild. ## Bundle root contents | Path | Description | | --- | --- | | `{executable}` | Compiled launcher for this operating system. | - | `data/` | Python runtime, application code, native libraries, and GPU assets. | + | Inside executable | Python runtime, application code, native libraries, and GPU assets. | | `cache/` | Model weights, scenes, compiled kernels, and native extensions. | + | `{_CACHE_ARCHIVE}` | Optional archived cache files, extracted into the writable cache on first launch. | + | `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. + PyInstaller extracts its embedded runtime files to a temporary directory + on each launch. Keep the executable with `cache/`, any + `cache-extras.zip`, and any packaged `artifacts/` assets. """ ) @@ -635,11 +669,9 @@ def _pyinstaller_command( "PyInstaller", "--noconfirm", "--clean", - "--onedir", + "--onefile", "--name", slug, - "--contents-directory", - "data", "--distpath", str(build_root / "dist"), "--workpath", @@ -648,6 +680,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", @@ -680,6 +715,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 +725,65 @@ 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 _archive_cache_to_file_limit(bundle_root: Path) -> None: + """Archive large cache subtrees until a onefile bundle meets its file limit.""" + file_count = sum(path.is_file() for path in bundle_root.rglob("*")) + if file_count <= _ONEFILE_MAX_FILES: + return + cache_root = bundle_root / "cache" + candidates = sorted( + ( + (directory, sum(path.is_file() for path in directory.rglob("*"))) + for directory in cache_root.iterdir() + if directory.is_dir() + ), + key=lambda entry: entry[1], + reverse=True, + ) + selected: list[Path] = [] + for directory, count in candidates: + if file_count + 1 <= _ONEFILE_MAX_FILES: + break + selected.append(directory) + file_count -= count + if not selected or file_count + 1 > _ONEFILE_MAX_FILES: + raise PackageError( + f"Cannot reduce the onefile bundle to {_ONEFILE_MAX_FILES} files " + "by archiving cache directories." + ) + + archive_path = bundle_root / _CACHE_ARCHIVE + with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_STORED) as archive: + for directory in selected: + for path in directory.rglob("*"): + archive.write(path, path.relative_to(cache_root)) + with zipfile.ZipFile(archive_path) as archive: + bad_file = archive.testzip() + if bad_file is not None: + raise PackageError(f"Cache archive verification failed at {bad_file}.") + for directory in selected: + shutil.rmtree(directory) + actual_count = sum(path.is_file() for path in bundle_root.rglob("*")) + if actual_count > _ONEFILE_MAX_FILES: + raise PackageError( + f"Onefile bundle contains {actual_count} files, above " + f"the {_ONEFILE_MAX_FILES}-file limit." + ) + + 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 {".", ".."}: @@ -755,6 +850,7 @@ def package( installer_output = staging / _INSTALLER_OUTPUT preparation_issues = staging / _PREPARATION_ISSUES succeeded = False + completed_bundle = False try: cache_root.mkdir() @@ -773,6 +869,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 +886,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) @@ -826,18 +923,14 @@ def package( ) if pyinstaller_result.returncode: raise PackageError( - f"PyInstaller failed with exit code {pyinstaller_result.returncode}." + f"PyInstaller failed with exit code {pyinstaller_result.returncode}.\n" + f"{_log_tail(installer_output)}" ) - 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." - ) - - for item in built_bundle.iterdir(): - shutil.move(str(item), staging / item.name) + executable = build_root / "dist" / executable_name + if not executable.is_file(): + raise PackageError("PyInstaller completed without the expected executable.") + shutil.move(str(executable), staging / executable_name) (staging / "README.md").write_text( _bundle_readme_source(slug), encoding="utf-8" ) @@ -845,11 +938,21 @@ def package( shutil.rmtree(build_root) launcher.unlink() preload.unlink() - staging.replace(destination) + _archive_cache_to_file_limit(staging) + completed_bundle = True + try: + if destination.exists(): + raise FileExistsError(f"Output appeared during build: {destination}") + staging.replace(destination) + except OSError as error: + raise PackageError( + f"Could not publish the completed bundle to {destination}: {error}. " + f"The bundle is retained at {staging}." + ) from error succeeded = True return destination finally: - if not succeeded: + if not succeeded and not completed_bundle: shutil.rmtree(staging, ignore_errors=True) 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 index 95cf33662..2f0ca1a1b 100644 --- a/tools/package-as-offline-exe/test_package_as_offline_exe.py +++ b/tools/package-as-offline-exe/test_package_as_offline_exe.py @@ -5,11 +5,14 @@ import os import runpy +import shutil +import subprocess import sys from pathlib import Path from typing import Any import pytest +from torch.utils.cpp_extension import verify_ninja_availability from flashdreams.runtime_v2 import cli @@ -48,6 +51,7 @@ def test_slangpy_shaders_are_bundled_beside_runtime_library( packager_globals, "_local_dependency_modules", lambda _distributions: () ) monkeypatch.setitem(packager_globals, "_nvrtc_builtins", lambda: ()) + ninja = _PACKAGER["_ninja_executable"]() command = _PACKAGER["_pyinstaller_command"]( launcher=tmp_path / "launcher.py", @@ -56,10 +60,23 @@ def test_slangpy_shaders_are_bundled_beside_runtime_library( build_root=tmp_path / "build", ) + assert "--onefile" in command + assert "--onedir" not in command + assert "--contents-directory" not in command add_data = command.index("--add-data") assert command[add_data + 1] == ( f"{tmp_path / 'slangpy' / 'shaders'}{os.pathsep}shaders" ) + assert f"{ninja}{os.pathsep}." in [ + command[index + 1] + for index, argument in enumerate(command) + if argument == "--add-binary" + ] + runtime_root = tmp_path / "data" + runtime_root.mkdir() + shutil.copy2(ninja, runtime_root / ninja.name) + monkeypatch.setenv("PATH", str(runtime_root)) + verify_ninja_availability() def test_nvrtc_builtins_are_bundled_beside_nvrtc( @@ -79,6 +96,9 @@ def test_nvrtc_builtins_are_bundled_beside_nvrtc( "_nvrtc_builtins", lambda: ((builtins, Path("nvidia/cu13/lib")),), ) + monkeypatch.setitem( + packager_globals, "_ninja_executable", lambda: tmp_path / "ninja.exe" + ) command = _PACKAGER["_pyinstaller_command"]( launcher=tmp_path / "launcher.py", @@ -169,11 +189,80 @@ def test_launcher_keeps_runtime_and_application_overrides_separate( ] +def test_frozen_launcher_runs_from_bundle_directory( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + bundle = tmp_path / "bundle" + bundle.mkdir() + launcher = bundle / "launcher.py" + launcher.write_text( + _PACKAGER["_launcher_source"]( + "example", ["--mode", "native-window"], "test-seed" + ), + encoding="utf-8", + ) + physx_source = bundle / "cache" / "ludus" / "physx" / "source" + physx_source.mkdir(parents=True) + for index in range(5): + (physx_source / f"file-{index}.txt").write_text("prepared", encoding="utf-8") + monkeypatch.setitem( + _PACKAGER["_archive_cache_to_file_limit"].__globals__, + "_ONEFILE_MAX_FILES", + 3, + ) + _PACKAGER["_archive_cache_to_file_limit"](bundle) + assert (bundle / "cache-extras.zip").is_file() + assert not (bundle / "cache" / "ludus").exists() + assert sum(path.is_file() for path in bundle.rglob("*")) <= 3 + stubs = tmp_path / "stubs" + for package in ("flashdreams", "flashdreams/runtime_v2", "torch", "torch/utils"): + directory = stubs / package + directory.mkdir(parents=True) + (directory / "__init__.py").touch() + (stubs / "torch/utils/cpp_extension.py").write_text( + "def load(*args, **kwargs): pass\n" + ) + (stubs / "flashdreams/runtime_v2/cli.py").write_text( + "import os\n" + "from pathlib import Path\n" + "def split_arguments(args): return args, []\n" + "def entrypoint(args):\n" + " assert Path.cwd() == Path(os.environ['EXPECTED_BUNDLE_DIR'])\n" + " assert (Path(os.environ['LUDUS_PHYSX_CACHE']) / 'source' / 'file-0.txt').read_text() == 'prepared'\n" + ) + outside = tmp_path / "outside" + outside.mkdir() + env = os.environ.copy() + env.update( + PYTHONPATH=str(stubs), + EXPECTED_BUNDLE_DIR=str(bundle), + FLASHDREAMS_RUNTIME_CACHE_DIR=str(tmp_path / "runtime-cache"), + ) + bootstrap = ( + "import runpy, sys\n" + "sys.frozen = True\n" + f"sys.executable = {str(bundle / 'example.exe')!r}\n" + f"sys._MEIPASS = {str(bundle / 'data')!r}\n" + f"runpy.run_path({str(launcher)!r})\n" + ) + result = subprocess.run( + [sys.executable, "-S", "-c", bootstrap], + cwd=outside, + env=env, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + + def test_preparation_issues_are_written_beside_installer_output( tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, ) -> None: staging = tmp_path / "bundle" issues_path = staging / "PREPARATION_ISSUES.txt" + monkeypatch.setenv("PATH", "") environment = _PACKAGER["_cache_environment"]( staging / "cache", @@ -182,3 +271,51 @@ def test_preparation_issues_are_written_beside_installer_output( assert environment["FLASHDREAMS_PREPARATION_ISSUES_PATH"] == str(issues_path) assert issues_path.parent == staging + assert shutil.which("ninja", path=environment["PATH"]) is not None + + +def test_preload_failure_reports_traceback( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + packager_globals = _PACKAGER["package"].__globals__ + + def failed_preload( + args: list[str], **kwargs: Any + ) -> subprocess.CompletedProcess[str]: + kwargs["stdout"].write( + b"Traceback (most recent call last):\nValueError: preload failed\n" + ) + return subprocess.CompletedProcess(args, 1) + + monkeypatch.setattr(packager_globals["subprocess"], "run", failed_preload) + with pytest.raises(_PACKAGER["PackageError"], match="ValueError: preload failed"): + _PACKAGER["package"]( + "example", ["--mode", "native-window"], tmp_path / "bundle" + ) + + +def test_completed_bundle_survives_output_race( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + packager_globals = _PACKAGER["package"].__globals__ + destination = tmp_path / "bundle" + + def fake_run(args: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + if args[1].endswith("preload.py"): + return subprocess.CompletedProcess(args, 0) + build = kwargs["cwd"] / ".build" / "dist" + build.mkdir(parents=True) + (build / _PACKAGER["_executable_name"]("example")).touch() + destination.mkdir() + return subprocess.CompletedProcess(args, 0) + + monkeypatch.setattr(packager_globals["subprocess"], "run", fake_run) + monkeypatch.setitem( + packager_globals, "_pyinstaller_command", lambda **_: ["python", "pyinstaller"] + ) + with pytest.raises(_PACKAGER["PackageError"], match="retained at"): + _PACKAGER["package"]("example", ["--mode", "native-window"], destination) + + staging = next(tmp_path.glob(".fd-*.tmp")) + assert (staging / _PACKAGER["_executable_name"]("example")).is_file() + assert not (staging / "data").exists() From ebe65e9d16e3df1437d3c98e3eaeeb874a004644 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Wed, 7 Oct 2026 17:04:37 -0700 Subject: [PATCH 02/16] clean up demo --- tools/package-as-offline-exe/README.md | 44 -------------------------- 1 file changed, 44 deletions(-) diff --git a/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index ccb778032..7557309cc 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -29,30 +29,10 @@ uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ --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. -To package the fast-perf Interactive Drive game mode: - -```powershell -uv run python tools/package-as-offline-exe/package_as_offline_exe.py ` - --output artifacts/interactive-drive-omnidreams-fast-perf-onefile-1700 ` - ::: flashdreams-run-v2 interactive-drive-omnidreams-fast-perf ` - --mode native-window -- --game-mode -``` - The output is still a directory. `cache/` holds the downloaded model weights and prepared kernels. When a onefile bundle would exceed 1,700 files, the packager stores the largest cache directories in `cache-extras.zip`. The @@ -63,30 +43,6 @@ overriding the default per-user cache. PyInstaller extracts the embedded runtime files to a temporary directory on each launch, which adds startup time. -On a Windows build of `interactive-drive-omnidreams-fast-perf` on 2026-10-07, -the completed bundle contained **98 files**: a 2.28 GiB EXE, a 0.47 GiB cache -archive, and 93 files in the 21.79 GiB `cache/`. A one-step native-window -game-mode run used the default per-user cache, restored the PhysX tree, rendered -five frames, and exited successfully in 136.1 seconds. This timing is -host-specific and includes model startup. - -## 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`. From 54c8b28ab93c8f2c763e14c3f62185c6029e0159 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Thu, 8 Oct 2026 17:09:25 -0700 Subject: [PATCH 03/16] pack fonts --- tools/package-as-offline-exe/README.md | 23 ++++--- .../package_as_offline_exe.py | 66 +++++++++++++------ .../test_package_as_offline_exe.py | 64 ++++++++++++++++-- 3 files changed, 118 insertions(+), 35 deletions(-) diff --git a/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index 7557309cc..424833e9b 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -38,8 +38,8 @@ and prepared kernels. When a onefile bundle would exceed 1,700 files, the packager stores the largest cache directories in `cache-extras.zip`. The launcher extracts that archive into the writable runtime cache on first launch; subsequent launches reuse it. Preload may also produce sibling `artifacts/` -assets. Set `FLASHDREAMS_RUNTIME_CACHE_DIR` to a path outside the bundle if -overriding the default per-user cache. +assets. Pass `--unpack-dir` to the packaged executable to change where its +cache is extracted. PyInstaller extracts the embedded runtime files to a temporary directory on each launch, which adds startup time. @@ -110,16 +110,21 @@ so relative application paths resolve beside the executable. ## 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: +the launcher copies it into a writable runtime 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` if a custom cache location is desired. +Pass `--unpack-dir PATH` to the packaged executable to use +`PATH//cache` instead. `PATH` must be absolute. For example, +`interactive-drive-omnidreams-fast-perf.exe --unpack-dir D:\FlashDreamsData`. +`FLASHDREAMS_RUNTIME_CACHE_DIR` remains available to set the exact cache +directory; the command-line option takes precedence. PyInstaller's onefile +runtime files still extract to the OS temp directory on every launch, before +the application can process `--unpack-dir`. On Windows, cache paths use the +extended-length path prefix so deep Hugging Face snapshots can be copied. Keep the executable with its sibling `cache/`, any `cache-extras.zip`, and any -`artifacts/` assets. Set `FLASHDREAMS_RUNTIME_CACHE_DIR` to move the writable -cache. +`artifacts/` assets. Pass `--unpack-dir` to move the writable cache. Destination machine still needs a compatible NVIDIA driver to run the packaged application. 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 501b3a6e5..2f198a3a5 100644 --- a/tools/package-as-offline-exe/package_as_offline_exe.py +++ b/tools/package-as-offline-exe/package_as_offline_exe.py @@ -398,6 +398,7 @@ 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 @@ -406,6 +407,20 @@ def _launcher_source( import zipfile from pathlib import Path + 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) @@ -414,35 +429,36 @@ def _launcher_source( 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() @@ -461,6 +477,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() @@ -542,7 +560,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}, @@ -601,9 +619,12 @@ 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. ## Expected processes during runtime @@ -647,6 +668,11 @@ def _pyinstaller_command( """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)) @@ -695,6 +721,8 @@ 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", 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 index 2f0ca1a1b..1ed86e11e 100644 --- a/tools/package-as-offline-exe/test_package_as_offline_exe.py +++ b/tools/package-as-offline-exe/test_package_as_offline_exe.py @@ -39,9 +39,13 @@ def test_noninteractive_mode_cannot_be_packaged() -> None: _PACKAGER["_validate_config"](["--mode", "mp4"]) -def test_slangpy_shaders_are_bundled_beside_runtime_library( +def test_required_runtime_assets_are_bundled( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: + imgui_fonts = tmp_path / "imgui_bundle/assets/fonts" + imgui_fonts.mkdir(parents=True) + (imgui_fonts / "DroidSans.ttf").write_bytes(b"font") + (imgui_fonts / "Inconsolata-Medium.ttf").write_bytes(b"second font") packager_globals = _PACKAGER["_pyinstaller_command"].__globals__ monkeypatch.setitem( packager_globals, "_module_search_path", lambda _module: tmp_path @@ -63,10 +67,13 @@ def test_slangpy_shaders_are_bundled_beside_runtime_library( assert "--onefile" in command assert "--onedir" not in command assert "--contents-directory" not in command - add_data = command.index("--add-data") - assert command[add_data + 1] == ( - f"{tmp_path / 'slangpy' / 'shaders'}{os.pathsep}shaders" - ) + bundled_data = [ + command[index + 1] + for index, argument in enumerate(command) + if argument == "--add-data" + ] + assert f"{tmp_path / 'slangpy' / 'shaders'}{os.pathsep}shaders" in bundled_data + assert f"{imgui_fonts}{os.pathsep}imgui_bundle/assets/fonts" in bundled_data assert f"{ninja}{os.pathsep}." in [ command[index + 1] for index, argument in enumerate(command) @@ -82,6 +89,9 @@ def test_slangpy_shaders_are_bundled_beside_runtime_library( def test_nvrtc_builtins_are_bundled_beside_nvrtc( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: + imgui_fonts = tmp_path / "imgui_bundle/assets/fonts" + imgui_fonts.mkdir(parents=True) + (imgui_fonts / "DroidSans.ttf").write_bytes(b"font") packager_globals = _PACKAGER["_pyinstaller_command"].__globals__ builtins = tmp_path / "nvidia" / "cu13" / "lib" / "libnvrtc-builtins.so.13.0" monkeypatch.setitem( @@ -189,8 +199,9 @@ def test_launcher_keeps_runtime_and_application_overrides_separate( ] +@pytest.mark.parametrize("custom_unpack_dir", [False, True]) def test_frozen_launcher_runs_from_bundle_directory( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, custom_unpack_dir: bool ) -> None: bundle = tmp_path / "bundle" bundle.mkdir() @@ -205,6 +216,23 @@ def test_frozen_launcher_runs_from_bundle_directory( physx_source.mkdir(parents=True) for index in range(5): (physx_source / f"file-{index}.txt").write_text("prepared", encoding="utf-8") + long_physx_name = "physx-" + "x" * 170 + ".txt" + long_physx_file = physx_source / long_physx_name + hub_relative = ( + Path("datasets--nvidia--omni-dreams-samples") + / "snapshots" + / ("c" * 40) + / "data" + / "single_view" + / "239560dc-33d1-11ef-9720-00044bcbccac" + / ("239560dc-33d1-11ef-9720-00044bcbccac_" + "1" * 70 + ".mp4") + ) + long_hf_file = bundle / "cache" / "huggingface" / "hub" / hub_relative + for path in (long_physx_file, long_hf_file): + filesystem_path = Path("\\\\?\\" + str(path)) if os.name == "nt" else path + filesystem_path.parent.mkdir(parents=True, exist_ok=True) + filesystem_path.write_text("prepared", encoding="utf-8") + assert len(str(long_hf_file)) > 260 monkeypatch.setitem( _PACKAGER["_archive_cache_to_file_limit"].__globals__, "_ONEFILE_MAX_FILES", @@ -228,21 +256,43 @@ def test_frozen_launcher_runs_from_bundle_directory( "def split_arguments(args): return args, []\n" "def entrypoint(args):\n" " assert Path.cwd() == Path(os.environ['EXPECTED_BUNDLE_DIR'])\n" + " assert '--unpack-dir' not in args\n" + " assert os.environ['HF_HUB_CACHE'].startswith(os.environ['EXPECTED_LONG_PATH_PREFIX'])\n" + " assert Path(os.environ['LUDUS_PHYSX_CACHE']).samefile(Path(os.environ['EXPECTED_CACHE_DIR']) / 'ludus' / 'physx')\n" " assert (Path(os.environ['LUDUS_PHYSX_CACHE']) / 'source' / 'file-0.txt').read_text() == 'prepared'\n" + " assert (Path(os.environ['LUDUS_PHYSX_CACHE']) / 'source' / os.environ['EXPECTED_LONG_PHYSX']).read_text() == 'prepared'\n" + " assert (Path(os.environ['HF_HUB_CACHE']) / os.environ['EXPECTED_LONG_HF']).read_text() == 'prepared'\n" ) outside = tmp_path / "outside" outside.mkdir() + profile = tmp_path / "profile" + profile.mkdir() + unpack_root = ( + tmp_path / "custom-unpack" + if custom_unpack_dir + else profile / "flashdreams" / "unpacked" + ) + cache_root = unpack_root / "example" / "cache" env = os.environ.copy() env.update( PYTHONPATH=str(stubs), EXPECTED_BUNDLE_DIR=str(bundle), - FLASHDREAMS_RUNTIME_CACHE_DIR=str(tmp_path / "runtime-cache"), + EXPECTED_CACHE_DIR=str(cache_root), + EXPECTED_LONG_PATH_PREFIX="\\\\?\\" if os.name == "nt" else "", + EXPECTED_LONG_PHYSX=long_physx_name, + EXPECTED_LONG_HF=str(hub_relative), + USERPROFILE=str(profile), + HOME=str(profile), ) + env.pop("FLASHDREAMS_RUNTIME_CACHE_DIR", None) + if custom_unpack_dir: + env["FLASHDREAMS_RUNTIME_CACHE_DIR"] = str(tmp_path / "env-cache") bootstrap = ( "import runpy, sys\n" "sys.frozen = True\n" f"sys.executable = {str(bundle / 'example.exe')!r}\n" f"sys._MEIPASS = {str(bundle / 'data')!r}\n" + f"sys.argv = {['example.exe', '--unpack-dir', str(unpack_root)] if custom_unpack_dir else ['example.exe']!r}\n" f"runpy.run_path({str(launcher)!r})\n" ) result = subprocess.run( From 3560a0bcdd566c198ec065c997226cbd0aad7b02 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Sat, 10 Oct 2026 11:47:33 -0700 Subject: [PATCH 04/16] prepare packages better for offline - especially for omnidreams --- .../flashdreams/core/checkpoint/load.py | 30 +++++++++++-------- flashdreams/tests/test_checkpoint_loading.py | 30 +++++++++++++++++++ .../ludus_renderer/_ops/_plugin.py | 3 +- .../impl/native/omnidreams_singleview.py | 7 ++--- tools/package-as-offline-exe/README.md | 4 +++ .../package_as_offline_exe.py | 11 +++---- .../test_package_as_offline_exe.py | 7 +++++ 7 files changed, 69 insertions(+), 23 deletions(-) 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/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/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/native/omnidreams_singleview.py b/integrations_v2/omnidreams/impl/native/omnidreams_singleview.py index 537b0b872..04a8ba033 100644 --- a/integrations_v2/omnidreams/impl/native/omnidreams_singleview.py +++ b/integrations_v2/omnidreams/impl/native/omnidreams_singleview.py @@ -552,13 +552,12 @@ def _scoped_cuda_arch_list(cuda_arch_list: str | None) -> Iterator[None]: os.environ[_PYTORCH_CUDA_ARCH_LIST_ENV] = previous -def _load_prebuilt_extension(sage3_disabled: bool) -> ModuleType: +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" - prefix = f"omnidreams_singleview_native_sage3_{int(not sage3_disabled)}_" binaries = [ (directory.name, binary) - for directory in build_root.glob(f"{prefix}*") + 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() ] @@ -608,7 +607,7 @@ def load_extension( try: if getattr(sys, "frozen", False): - _extension[extension_key] = _load_prebuilt_extension(sage3_disabled) + _extension[extension_key] = _load_prebuilt_extension() return _extension[extension_key] _ensure_windows_cuda13_toolkit() diff --git a/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index 424833e9b..ff0ef1e7b 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -91,6 +91,10 @@ The executable contains the Python runtime, application code, dependencies, native libraries, Ninja, and packaged GPU assets. The bundle has at most 1,700 files; `cache-extras.zip` holds cache files moved out of the directory tree to meet that limit. +Native extensions are compiled during packaging and reused at runtime. The +destination machine does not need CUDA Toolkit or Visual Studio Build Tools +for those extensions, but it still needs a compatible NVIDIA GPU and driver. +If a prepared extension is missing, rebuild the bundle on the packaging host. Notes on generated files: 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 2f198a3a5..381bd18eb 100644 --- a/tools/package-as-offline-exe/package_as_offline_exe.py +++ b/tools/package-as-offline-exe/package_as_offline_exe.py @@ -517,8 +517,6 @@ def filesystem_path(path): # 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): @@ -533,7 +531,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 @@ -633,8 +634,8 @@ def _bundle_readme_source(slug: str) -> str: | `{executable}` | 2 | One process extracts and later cleans up the runtime; the other 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. Ninja - is included if a native extension needs to rebuild. + 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 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 index 1ed86e11e..4370a38bf 100644 --- a/tools/package-as-offline-exe/test_package_as_offline_exe.py +++ b/tools/package-as-offline-exe/test_package_as_offline_exe.py @@ -255,6 +255,13 @@ def test_frozen_launcher_runs_from_bundle_directory( "from pathlib import Path\n" "def split_arguments(args): return args, []\n" "def entrypoint(args):\n" + " from torch.utils import cpp_extension\n" + " try:\n" + " cpp_extension.load('missing_extension')\n" + " except RuntimeError as error:\n" + " assert 'Preloaded native extension' in str(error)\n" + " else:\n" + " raise AssertionError('frozen launcher attempted native compilation')\n" " assert Path.cwd() == Path(os.environ['EXPECTED_BUNDLE_DIR'])\n" " assert '--unpack-dir' not in args\n" " assert os.environ['HF_HUB_CACHE'].startswith(os.environ['EXPECTED_LONG_PATH_PREFIX'])\n" From eec72243803430b5c3f9c2ab36a48d9edecd5802 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Sun, 11 Oct 2026 21:39:40 -0700 Subject: [PATCH 05/16] add noconsole to pyinstaller packager --- tools/package-as-offline-exe/README.md | 9 ++++--- .../package_as_offline_exe.py | 12 +++++++++ .../test_package_as_offline_exe.py | 25 ++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index ff0ef1e7b..8b93c43d0 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -12,9 +12,9 @@ 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-fast-perf \ - --mode native-window -- --game-mode + --noconsole --output artifacts/taxi-omnidreams-bundle \ + ::: flashdreams-run-v2 crazy-robotaxi-omnidreams-fast-perf \ + --mode native-window ``` The command after `:::` must begin with `flashdreams-run-v2`, and its effective @@ -53,6 +53,9 @@ written to `PREPARATION_ISSUES.txt` beside `INSTALLER_OUTPUT.txt`. --output PATH Destination bundle directory. +--noconsole + Build without a console window on Windows (ignored on Linux). + --preload-timeout SECONDS Fail if initialization and preload do not finish within this time. 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 381bd18eb..02cdb54c8 100644 --- a/tools/package-as-offline-exe/package_as_offline_exe.py +++ b/tools/package-as-offline-exe/package_as_offline_exe.py @@ -90,6 +90,11 @@ def _parser() -> argparse.ArgumentParser: type=Path, help="Bundle directory (default: artifacts/-bundle).", ) + 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, @@ -665,6 +670,7 @@ 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] @@ -729,6 +735,8 @@ def _pyinstaller_command( "--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)) @@ -835,6 +843,7 @@ def package( 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``. @@ -846,6 +855,7 @@ def package( 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. @@ -940,6 +950,7 @@ def package( build_root=build_root, hidden_imports=hidden_imports, executables=executables, + noconsole=noconsole, ), cwd=staging, env=_cache_environment(cache_root), @@ -1001,6 +1012,7 @@ 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, ) 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 index 4370a38bf..5cb4754f6 100644 --- a/tools/package-as-offline-exe/test_package_as_offline_exe.py +++ b/tools/package-as-offline-exe/test_package_as_offline_exe.py @@ -39,8 +39,9 @@ def test_noninteractive_mode_cannot_be_packaged() -> None: _PACKAGER["_validate_config"](["--mode", "mp4"]) +@pytest.mark.parametrize("noconsole", [False, True]) def test_required_runtime_assets_are_bundled( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, noconsole: bool ) -> None: imgui_fonts = tmp_path / "imgui_bundle/assets/fonts" imgui_fonts.mkdir(parents=True) @@ -62,9 +63,11 @@ def test_required_runtime_assets_are_bundled( slug="example", application_module="example", build_root=tmp_path / "build", + noconsole=noconsole, ) assert "--onefile" in command + assert ("--noconsole" in command) == noconsole assert "--onedir" not in command assert "--contents-directory" not in command bundled_data = [ @@ -126,6 +129,26 @@ def test_nvrtc_builtins_are_bundled_beside_nvrtc( assert f"{builtins}{os.pathsep}{Path('nvidia/cu13/lib')}" in binaries +@pytest.mark.parametrize("noconsole", [False, True]) +def test_main_passes_noconsole_to_package( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, noconsole: bool +) -> None: + packaged_options: dict[str, Any] = {} + + def fake_package(*args: Any, **kwargs: Any) -> Path: + packaged_options.update(kwargs) + return tmp_path + + monkeypatch.setitem(_PACKAGER["main"].__globals__, "package", fake_package) + arguments = ["--noconsole"] if noconsole else [] + arguments.extend( + [":::", "flashdreams-run-v2", "example", "--mode", "native-window"] + ) + + assert _PACKAGER["main"](arguments) == 0 + assert packaged_options["noconsole"] == noconsole + + def test_skip_runtime_validation_keeps_application_preload() -> None: parsed, command = _PACKAGER["_parse_arguments"]( [ From 8586cacbbbcdbbb2f716776e39d8c82dc07e6557 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Mon, 12 Oct 2026 13:45:59 -0700 Subject: [PATCH 06/16] fix noconsole bug, add parameter to packager to manage working directory: `--working-dir` --- apps/t2v/tests/test_structure.py | 123 ------ .../test_v2/test_webrtc_client_window.py | 1 - flashdreams/tests/test_benchmark_harness.py | 148 ------- flashdreams/tests/test_multiview_mask.py | 9 - flashdreams/tests/test_multiview_prefill.py | 9 - flashdreams/tests/test_multiview_rollout.py | 21 - .../omnidreams/tests/test_recipe_configs.py | 48 --- tools/package-as-offline-exe/README.md | 4 + .../check_launcher_streams.py | 56 +++ .../package_as_offline_exe.py | 61 ++- .../test_package_as_offline_exe.py | 401 ------------------ 11 files changed, 108 insertions(+), 773 deletions(-) delete mode 100644 apps/t2v/tests/test_structure.py create mode 100644 tools/package-as-offline-exe/check_launcher_streams.py delete mode 100644 tools/package-as-offline-exe/test_package_as_offline_exe.py 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/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_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/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 8b93c43d0..68c6ab1f4 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -53,8 +53,12 @@ written to `PREPARATION_ISSUES.txt` beside `INSTALLER_OUTPUT.txt`. --output PATH Destination bundle directory. +--working-dir PATH + Defaults to the output directory (which must not already exist, ensuring nothing extra is copied if this option is used). Create if missing and copy its contents into the bundle before preload. + --noconsole Build without a console window on Windows (ignored on Linux). + Console-free launchers write stdout and stderr to runtime.log in the writable runtime cache. --preload-timeout SECONDS Fail if initialization and preload do not finish within this time. 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..78d86937a --- /dev/null +++ b/tools/package-as-offline-exe/check_launcher_streams.py @@ -0,0 +1,56 @@ +# 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 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.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) / "cache" / "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() + +print("Launcher stream 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 02cdb54c8..abcffa36e 100644 --- a/tools/package-as-offline-exe/package_as_offline_exe.py +++ b/tools/package-as-offline-exe/package_as_offline_exe.py @@ -90,6 +90,11 @@ 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", @@ -506,6 +511,15 @@ def filesystem_path(path): cache_path.mkdir(parents=True, exist_ok=True) os.environ[name] = str(cache_path) + if sys.stdout is None or sys.stderr is None: + runtime_log = (cache_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 + if getattr(sys, "frozen", False): runtime_root = Path(getattr(sys, "_MEIPASS", bundle_root)) os.environ["PATH"] = ( @@ -839,6 +853,7 @@ 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, ...] = (), @@ -851,6 +866,7 @@ 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. @@ -880,7 +896,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", + _CACHE_ARCHIVE, + ".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" @@ -889,9 +927,15 @@ def package( installer_output = staging / _INSTALLER_OUTPUT preparation_issues = staging / _PREPARATION_ISSUES succeeded = False - completed_bundle = 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( @@ -979,20 +1023,10 @@ def package( launcher.unlink() preload.unlink() _archive_cache_to_file_limit(staging) - completed_bundle = True - try: - if destination.exists(): - raise FileExistsError(f"Output appeared during build: {destination}") - staging.replace(destination) - except OSError as error: - raise PackageError( - f"Could not publish the completed bundle to {destination}: {error}. " - f"The bundle is retained at {staging}." - ) from error succeeded = True return destination finally: - if not succeeded and not completed_bundle: + if not succeeded: shutil.rmtree(staging, ignore_errors=True) @@ -1013,6 +1047,7 @@ def main(arguments: list[str] | None = None) -> int: 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 5cb4754f6..000000000 --- a/tools/package-as-offline-exe/test_package_as_offline_exe.py +++ /dev/null @@ -1,401 +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 shutil -import subprocess -import sys -from pathlib import Path -from typing import Any - -import pytest -from torch.utils.cpp_extension import verify_ninja_availability - -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"]) - - -@pytest.mark.parametrize("noconsole", [False, True]) -def test_required_runtime_assets_are_bundled( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, noconsole: bool -) -> None: - imgui_fonts = tmp_path / "imgui_bundle/assets/fonts" - imgui_fonts.mkdir(parents=True) - (imgui_fonts / "DroidSans.ttf").write_bytes(b"font") - (imgui_fonts / "Inconsolata-Medium.ttf").write_bytes(b"second font") - 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: ()) - ninja = _PACKAGER["_ninja_executable"]() - - command = _PACKAGER["_pyinstaller_command"]( - launcher=tmp_path / "launcher.py", - slug="example", - application_module="example", - build_root=tmp_path / "build", - noconsole=noconsole, - ) - - assert "--onefile" in command - assert ("--noconsole" in command) == noconsole - assert "--onedir" not in command - assert "--contents-directory" not in command - bundled_data = [ - command[index + 1] - for index, argument in enumerate(command) - if argument == "--add-data" - ] - assert f"{tmp_path / 'slangpy' / 'shaders'}{os.pathsep}shaders" in bundled_data - assert f"{imgui_fonts}{os.pathsep}imgui_bundle/assets/fonts" in bundled_data - assert f"{ninja}{os.pathsep}." in [ - command[index + 1] - for index, argument in enumerate(command) - if argument == "--add-binary" - ] - runtime_root = tmp_path / "data" - runtime_root.mkdir() - shutil.copy2(ninja, runtime_root / ninja.name) - monkeypatch.setenv("PATH", str(runtime_root)) - verify_ninja_availability() - - -def test_nvrtc_builtins_are_bundled_beside_nvrtc( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - imgui_fonts = tmp_path / "imgui_bundle/assets/fonts" - imgui_fonts.mkdir(parents=True) - (imgui_fonts / "DroidSans.ttf").write_bytes(b"font") - 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")),), - ) - monkeypatch.setitem( - packager_globals, "_ninja_executable", lambda: tmp_path / "ninja.exe" - ) - - 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 - - -@pytest.mark.parametrize("noconsole", [False, True]) -def test_main_passes_noconsole_to_package( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, noconsole: bool -) -> None: - packaged_options: dict[str, Any] = {} - - def fake_package(*args: Any, **kwargs: Any) -> Path: - packaged_options.update(kwargs) - return tmp_path - - monkeypatch.setitem(_PACKAGER["main"].__globals__, "package", fake_package) - arguments = ["--noconsole"] if noconsole else [] - arguments.extend( - [":::", "flashdreams-run-v2", "example", "--mode", "native-window"] - ) - - assert _PACKAGER["main"](arguments) == 0 - assert packaged_options["noconsole"] == noconsole - - -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", - ] - ] - - -@pytest.mark.parametrize("custom_unpack_dir", [False, True]) -def test_frozen_launcher_runs_from_bundle_directory( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, custom_unpack_dir: bool -) -> None: - bundle = tmp_path / "bundle" - bundle.mkdir() - launcher = bundle / "launcher.py" - launcher.write_text( - _PACKAGER["_launcher_source"]( - "example", ["--mode", "native-window"], "test-seed" - ), - encoding="utf-8", - ) - physx_source = bundle / "cache" / "ludus" / "physx" / "source" - physx_source.mkdir(parents=True) - for index in range(5): - (physx_source / f"file-{index}.txt").write_text("prepared", encoding="utf-8") - long_physx_name = "physx-" + "x" * 170 + ".txt" - long_physx_file = physx_source / long_physx_name - hub_relative = ( - Path("datasets--nvidia--omni-dreams-samples") - / "snapshots" - / ("c" * 40) - / "data" - / "single_view" - / "239560dc-33d1-11ef-9720-00044bcbccac" - / ("239560dc-33d1-11ef-9720-00044bcbccac_" + "1" * 70 + ".mp4") - ) - long_hf_file = bundle / "cache" / "huggingface" / "hub" / hub_relative - for path in (long_physx_file, long_hf_file): - filesystem_path = Path("\\\\?\\" + str(path)) if os.name == "nt" else path - filesystem_path.parent.mkdir(parents=True, exist_ok=True) - filesystem_path.write_text("prepared", encoding="utf-8") - assert len(str(long_hf_file)) > 260 - monkeypatch.setitem( - _PACKAGER["_archive_cache_to_file_limit"].__globals__, - "_ONEFILE_MAX_FILES", - 3, - ) - _PACKAGER["_archive_cache_to_file_limit"](bundle) - assert (bundle / "cache-extras.zip").is_file() - assert not (bundle / "cache" / "ludus").exists() - assert sum(path.is_file() for path in bundle.rglob("*")) <= 3 - stubs = tmp_path / "stubs" - for package in ("flashdreams", "flashdreams/runtime_v2", "torch", "torch/utils"): - directory = stubs / package - directory.mkdir(parents=True) - (directory / "__init__.py").touch() - (stubs / "torch/utils/cpp_extension.py").write_text( - "def load(*args, **kwargs): pass\n" - ) - (stubs / "flashdreams/runtime_v2/cli.py").write_text( - "import os\n" - "from pathlib import Path\n" - "def split_arguments(args): return args, []\n" - "def entrypoint(args):\n" - " from torch.utils import cpp_extension\n" - " try:\n" - " cpp_extension.load('missing_extension')\n" - " except RuntimeError as error:\n" - " assert 'Preloaded native extension' in str(error)\n" - " else:\n" - " raise AssertionError('frozen launcher attempted native compilation')\n" - " assert Path.cwd() == Path(os.environ['EXPECTED_BUNDLE_DIR'])\n" - " assert '--unpack-dir' not in args\n" - " assert os.environ['HF_HUB_CACHE'].startswith(os.environ['EXPECTED_LONG_PATH_PREFIX'])\n" - " assert Path(os.environ['LUDUS_PHYSX_CACHE']).samefile(Path(os.environ['EXPECTED_CACHE_DIR']) / 'ludus' / 'physx')\n" - " assert (Path(os.environ['LUDUS_PHYSX_CACHE']) / 'source' / 'file-0.txt').read_text() == 'prepared'\n" - " assert (Path(os.environ['LUDUS_PHYSX_CACHE']) / 'source' / os.environ['EXPECTED_LONG_PHYSX']).read_text() == 'prepared'\n" - " assert (Path(os.environ['HF_HUB_CACHE']) / os.environ['EXPECTED_LONG_HF']).read_text() == 'prepared'\n" - ) - outside = tmp_path / "outside" - outside.mkdir() - profile = tmp_path / "profile" - profile.mkdir() - unpack_root = ( - tmp_path / "custom-unpack" - if custom_unpack_dir - else profile / "flashdreams" / "unpacked" - ) - cache_root = unpack_root / "example" / "cache" - env = os.environ.copy() - env.update( - PYTHONPATH=str(stubs), - EXPECTED_BUNDLE_DIR=str(bundle), - EXPECTED_CACHE_DIR=str(cache_root), - EXPECTED_LONG_PATH_PREFIX="\\\\?\\" if os.name == "nt" else "", - EXPECTED_LONG_PHYSX=long_physx_name, - EXPECTED_LONG_HF=str(hub_relative), - USERPROFILE=str(profile), - HOME=str(profile), - ) - env.pop("FLASHDREAMS_RUNTIME_CACHE_DIR", None) - if custom_unpack_dir: - env["FLASHDREAMS_RUNTIME_CACHE_DIR"] = str(tmp_path / "env-cache") - bootstrap = ( - "import runpy, sys\n" - "sys.frozen = True\n" - f"sys.executable = {str(bundle / 'example.exe')!r}\n" - f"sys._MEIPASS = {str(bundle / 'data')!r}\n" - f"sys.argv = {['example.exe', '--unpack-dir', str(unpack_root)] if custom_unpack_dir else ['example.exe']!r}\n" - f"runpy.run_path({str(launcher)!r})\n" - ) - result = subprocess.run( - [sys.executable, "-S", "-c", bootstrap], - cwd=outside, - env=env, - capture_output=True, - text=True, - check=False, - ) - assert result.returncode == 0, result.stderr - - -def test_preparation_issues_are_written_beside_installer_output( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - staging = tmp_path / "bundle" - issues_path = staging / "PREPARATION_ISSUES.txt" - monkeypatch.setenv("PATH", "") - - 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 - assert shutil.which("ninja", path=environment["PATH"]) is not None - - -def test_preload_failure_reports_traceback( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - packager_globals = _PACKAGER["package"].__globals__ - - def failed_preload( - args: list[str], **kwargs: Any - ) -> subprocess.CompletedProcess[str]: - kwargs["stdout"].write( - b"Traceback (most recent call last):\nValueError: preload failed\n" - ) - return subprocess.CompletedProcess(args, 1) - - monkeypatch.setattr(packager_globals["subprocess"], "run", failed_preload) - with pytest.raises(_PACKAGER["PackageError"], match="ValueError: preload failed"): - _PACKAGER["package"]( - "example", ["--mode", "native-window"], tmp_path / "bundle" - ) - - -def test_completed_bundle_survives_output_race( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - packager_globals = _PACKAGER["package"].__globals__ - destination = tmp_path / "bundle" - - def fake_run(args: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: - if args[1].endswith("preload.py"): - return subprocess.CompletedProcess(args, 0) - build = kwargs["cwd"] / ".build" / "dist" - build.mkdir(parents=True) - (build / _PACKAGER["_executable_name"]("example")).touch() - destination.mkdir() - return subprocess.CompletedProcess(args, 0) - - monkeypatch.setattr(packager_globals["subprocess"], "run", fake_run) - monkeypatch.setitem( - packager_globals, "_pyinstaller_command", lambda **_: ["python", "pyinstaller"] - ) - with pytest.raises(_PACKAGER["PackageError"], match="retained at"): - _PACKAGER["package"]("example", ["--mode", "native-window"], destination) - - staging = next(tmp_path.glob(".fd-*.tmp")) - assert (staging / _PACKAGER["_executable_name"]("example")).is_file() - assert not (staging / "data").exists() From 9b6c4a9cf2445e508414649ab7a33f5e90c55ce4 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Mon, 12 Oct 2026 23:30:09 -0700 Subject: [PATCH 07/16] no longer have archive/file-limit --- .../tools/offline_program_packager.md | 25 +++-- tools/package-as-offline-exe/README.md | 39 ++++--- .../check_launcher_streams.py | 33 +++++- .../package_as_offline_exe.py | 100 +++++------------- 4 files changed, 92 insertions(+), 105 deletions(-) diff --git a/docs/source/documentation/tools/offline_program_packager.md b/docs/source/documentation/tools/offline_program_packager.md index ae4b5a517..be527f957 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 @@ -79,32 +79,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/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index 68c6ab1f4..a4a6daaae 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -1,7 +1,7 @@ # Package a FlashDreams application for offline use `package_as_offline_exe.py` prepares an installed FlashDreams v2 application -and creates a single executable with its offline cache in one folder. +and creates a raw folder containing an executable, its runtime, and offline cache. Packaging is supported for applications running in `native-window` or `webrtc` mode. @@ -33,15 +33,14 @@ If `--output` is omitted, the default is `artifacts/-bundle`. The output directory must not already exist. -The output is still a directory. `cache/` holds the downloaded model weights -and prepared kernels. When a onefile bundle would exceed 1,700 files, the -packager stores the largest cache directories in `cache-extras.zip`. The -launcher extracts that archive into the writable runtime cache on first launch; -subsequent launches reuse it. Preload may also produce sibling `artifacts/` -assets. Pass `--unpack-dir` to the packaged executable to change where its -cache is extracted. -PyInstaller extracts the embedded runtime files to a temporary directory on -each launch, which adds startup time. +The executable embeds Python bytecode, which is read directly without extraction. +`_internal/` contains the Python runtime, dependencies, and native libraries. +The standard-library archive is also read in place. `cache/` holds downloaded +model weights and prepared kernels. Preload may also produce sibling `artifacts/` +assets. The packager uses PyInstaller's directory mode and does not extract +runtime files to a temporary directory at launch or archive the cache. +Deploy the complete folder as raw files. +Pass `--unpack-dir` to change where the writable data cache is copied. The packager runs initialization and one model block. Preparation findings are @@ -86,18 +85,17 @@ The generated directory has this shape: ```text / |-- [.exe] +|-- _internal/ |-- cache/ -|-- cache-extras.zip # when needed to meet the file limit |-- artifacts/ # when preload produces application assets |-- INSTALLER_OUTPUT.txt |-- PREPARATION_ISSUES.txt # only when preload warnings are found `-- README.md ``` -The executable contains the Python runtime, application code, dependencies, -native libraries, Ninja, and packaged GPU assets. The bundle has at most -1,700 files; `cache-extras.zip` holds cache files moved out of the directory -tree to meet that limit. +The executable contains embedded Python bytecode. `_internal/` contains the +Python runtime, supporting source files, dependencies, native libraries, Ninja, +and packaged GPU assets. Native extensions are compiled during packaging and reused at runtime. The destination machine does not need CUDA Toolkit or Visual Studio Build Tools for those extensions, but it still needs a compatible NVIDIA GPU and driver. @@ -130,12 +128,13 @@ Pass `--unpack-dir PATH` to the packaged executable to use `PATH//cache` instead. `PATH` must be absolute. For example, `interactive-drive-omnidreams-fast-perf.exe --unpack-dir D:\FlashDreamsData`. `FLASHDREAMS_RUNTIME_CACHE_DIR` remains available to set the exact cache -directory; the command-line option takes precedence. PyInstaller's onefile -runtime files still extract to the OS temp directory on every launch, before -the application can process `--unpack-dir`. On Windows, cache paths use the +directory; the command-line option takes precedence. Python and runtime DLLs +load directly from `_internal/`. Prepared C++, Triton, and PhysX extensions load from +the bundled `cache/`, even when the writable cache is relocated. +On Windows, cache paths use the extended-length path prefix so deep Hugging Face snapshots can be copied. -Keep the executable with its sibling `cache/`, any `cache-extras.zip`, and any -`artifacts/` assets. Pass `--unpack-dir` to move the writable cache. +Keep the executable with its sibling `_internal/`, `cache/`, and any +`artifacts/` assets. Pass `--unpack-dir` to move the writable data cache. Destination machine still needs a compatible NVIDIA driver to run the packaged application. diff --git a/tools/package-as-offline-exe/check_launcher_streams.py b/tools/package-as-offline-exe/check_launcher_streams.py index 78d86937a..bc9119fd7 100644 --- a/tools/package-as-offline-exe/check_launcher_streams.py +++ b/tools/package-as-offline-exe/check_launcher_streams.py @@ -7,6 +7,7 @@ import sys from pathlib import Path from tempfile import TemporaryDirectory +from types import SimpleNamespace from typing import Any from unittest.mock import patch @@ -53,4 +54,34 @@ if "runtime_log" in namespace: namespace["runtime_log"].close() -print("Launcher stream checks passed.") +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 abcffa36e..59e55e6c3 100644 --- a/tools/package-as-offline-exe/package_as_offline_exe.py +++ b/tools/package-as-offline-exe/package_as_offline_exe.py @@ -15,7 +15,6 @@ import subprocess import sys import uuid -import zipfile from importlib.metadata import ( PackageNotFoundError, distribution, @@ -40,8 +39,6 @@ _PREPARATION_ISSUES = "PREPARATION_ISSUES.txt" _RUNTIME_CACHE_ROOT_ENV = "FLASHDREAMS_RUNTIME_CACHE_DIR" _RUNTIME_CACHE_SEED_MARKER = ".flashdreams-cache-seed" -_CACHE_ARCHIVE = "cache-extras.zip" -_ONEFILE_MAX_FILES = 1700 _SUPPORTED_MODES = ("native-window", "webrtc") _CACHE_PATHS = { @@ -414,7 +411,6 @@ def _launcher_source( import os import shutil import sys - import zipfile from pathlib import Path def filesystem_path(path): @@ -473,12 +469,6 @@ def filesystem_path(path): cache_root = cache_root.resolve() seed_cache_root = seed_cache_root.resolve() - cache_archive = bundle_root / {_CACHE_ARCHIVE!r} - if cache_archive.is_file() and cache_root == seed_cache_root: - raise RuntimeError( - f"{_RUNTIME_CACHE_ROOT_ENV} must point outside the bundle " - "when cache-extras.zip is present." - ) if cache_root != seed_cache_root: if cache_root.is_relative_to( seed_cache_root @@ -495,13 +485,6 @@ def filesystem_path(path): or marker.read_text(encoding="utf-8") != cache_seed_id ): shutil.copytree(seed_cache_root, cache_root, dirs_exist_ok=True) - if cache_archive.is_file(): - with zipfile.ZipFile(cache_archive) as archive: - for member in archive.namelist(): - relative = Path(member) - if relative.is_absolute() or ".." in relative.parts: - raise RuntimeError(f"Unsafe cache archive path: {{member}}") - archive.extractall(cache_root) marker.write_text(cache_seed_id, encoding="utf-8") cache_paths = {_CACHE_PATHS!r} @@ -521,6 +504,13 @@ def filesystem_path(path): sys.stderr = runtime_log 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", "") @@ -564,7 +554,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") ) @@ -650,7 +642,7 @@ def _bundle_readme_source(slug: str) -> str: | Process | Expected count | Purpose | | --- | ---: | --- | - | `{executable}` | 2 | One process extracts and later cleans up the runtime; the other hosts the model loop and 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. @@ -660,18 +652,19 @@ def _bundle_readme_source(slug: str) -> str: | Path | Description | | --- | --- | - | `{executable}` | Compiled launcher for this operating system. | - | Inside executable | 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. | - | `{_CACHE_ARCHIVE}` | Optional archived cache files, extracted into the writable cache on first launch. | | `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. | - PyInstaller extracts its embedded runtime files to a temporary directory - on each launch. Keep the executable with `cache/`, any - `cache-extras.zip`, and any packaged `artifacts/` assets. + 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. """ ) @@ -716,7 +709,7 @@ def _pyinstaller_command( "PyInstaller", "--noconfirm", "--clean", - "--onefile", + "--onedir", "--name", slug, "--distpath", @@ -789,52 +782,6 @@ def _ninja_executable() -> Path: return Path(executable).resolve() -def _archive_cache_to_file_limit(bundle_root: Path) -> None: - """Archive large cache subtrees until a onefile bundle meets its file limit.""" - file_count = sum(path.is_file() for path in bundle_root.rglob("*")) - if file_count <= _ONEFILE_MAX_FILES: - return - cache_root = bundle_root / "cache" - candidates = sorted( - ( - (directory, sum(path.is_file() for path in directory.rglob("*"))) - for directory in cache_root.iterdir() - if directory.is_dir() - ), - key=lambda entry: entry[1], - reverse=True, - ) - selected: list[Path] = [] - for directory, count in candidates: - if file_count + 1 <= _ONEFILE_MAX_FILES: - break - selected.append(directory) - file_count -= count - if not selected or file_count + 1 > _ONEFILE_MAX_FILES: - raise PackageError( - f"Cannot reduce the onefile bundle to {_ONEFILE_MAX_FILES} files " - "by archiving cache directories." - ) - - archive_path = bundle_root / _CACHE_ARCHIVE - with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_STORED) as archive: - for directory in selected: - for path in directory.rglob("*"): - archive.write(path, path.relative_to(cache_root)) - with zipfile.ZipFile(archive_path) as archive: - bad_file = archive.testzip() - if bad_file is not None: - raise PackageError(f"Cache archive verification failed at {bad_file}.") - for directory in selected: - shutil.rmtree(directory) - actual_count = sum(path.is_file() for path in bundle_root.rglob("*")) - if actual_count > _ONEFILE_MAX_FILES: - raise PackageError( - f"Onefile bundle contains {actual_count} files, above " - f"the {_ONEFILE_MAX_FILES}-file limit." - ) - - 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 {".", ".."}: @@ -908,7 +855,7 @@ def package( working_dir.mkdir(parents=True, exist_ok=True) reserved = { "cache", - _CACHE_ARCHIVE, + "_internal", ".build", "launcher.py", "preload.py", @@ -1011,10 +958,12 @@ def package( f"{_log_tail(installer_output)}" ) - executable = build_root / "dist" / executable_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.") - shutil.move(str(executable), staging / executable_name) + 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" ) @@ -1022,7 +971,6 @@ def package( shutil.rmtree(build_root) launcher.unlink() preload.unlink() - _archive_cache_to_file_limit(staging) succeeded = True return destination finally: From 5b7f844778a60648ff1a52ef54b6c3d89212d03f Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Tue, 13 Oct 2026 15:45:51 -0700 Subject: [PATCH 08/16] remove noconsole due to it causing failures? --- tools/package-as-offline-exe/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index a4a6daaae..ef4aa733f 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -12,8 +12,8 @@ complete `flashdreams-run-v2` command: ```bash uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ - --noconsole --output artifacts/taxi-omnidreams-bundle \ - ::: flashdreams-run-v2 crazy-robotaxi-omnidreams-fast-perf \ + --output artifacts/taxi-omnidreams-bundle \ + ::: flashdreams-run-v2 crazy-robotaxi-omnidreams-fast-perf-responsive \ --mode native-window ``` From 79749aa5936fda421f6c1c551993b9c32ee1e308 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Tue, 13 Oct 2026 19:27:23 -0700 Subject: [PATCH 09/16] change default --- tools/package-as-offline-exe/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index ef4aa733f..872bbefd0 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -13,8 +13,8 @@ complete `flashdreams-run-v2` command: ```bash uv run python tools/package-as-offline-exe/package_as_offline_exe.py \ --output artifacts/taxi-omnidreams-bundle \ - ::: flashdreams-run-v2 crazy-robotaxi-omnidreams-fast-perf-responsive \ - --mode native-window + ::: flashdreams-run-v2 crazy-robotaxi-omnidreams-fast-perf \ + --mode native-window --fullscreen ``` The command after `:::` must begin with `flashdreams-run-v2`, and its effective From cd18882de9f63b6574a4eb108744686456c4b90b Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Wed, 14 Oct 2026 08:49:54 -0700 Subject: [PATCH 10/16] add optimizations that do not hurt quality to crazy-robotaxi; needed for windows to hit 28-29fps on 6000 pro --- .../ludus_renderer/_cpp/render/ludus_cuda.cu | 31 ++++++++++++------- .../ludus_renderer/_cpp/render/ludus_cuda.h | 1 + .../ludus_renderer/object_graph.py | 11 ++++--- .../tests/test_cudaraster_api.py | 1 + 4 files changed, 28 insertions(+), 16 deletions(-) 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/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/ludus-renderer/tests/test_cudaraster_api.py b/integrations_v2/omnidreams/impl/ludus-renderer/tests/test_cudaraster_api.py index e8c3b6845..fc6a400c7 100644 --- a/integrations_v2/omnidreams/impl/ludus-renderer/tests/test_cudaraster_api.py +++ b/integrations_v2/omnidreams/impl/ludus-renderer/tests/test_cudaraster_api.py @@ -2670,6 +2670,7 @@ def test_mixed_scale_overlap_respects_depth_order(harness: CudaRasterHarness) -> assert near_depth < far_depth + def test_timestamped_context_batches_distinct_timestamps_exactly() -> None: """A multi-frame call must match independent native submissions bit-for-bit.""" from ludus_renderer import ( From 198ec3fdf438d3c0b015064d44fdf4542b3f73ba Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Wed, 14 Oct 2026 09:22:15 -0700 Subject: [PATCH 11/16] faster math + format --- .../simulation/map_traffic.py | 65 +++++++++++++------ .../tests/test_cudaraster_api.py | 1 - 2 files changed, 44 insertions(+), 22 deletions(-) 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..f955aebb8 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), @@ -510,41 +534,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) 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 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/integrations_v2/omnidreams/impl/ludus-renderer/tests/test_cudaraster_api.py b/integrations_v2/omnidreams/impl/ludus-renderer/tests/test_cudaraster_api.py index fc6a400c7..e8c3b6845 100644 --- a/integrations_v2/omnidreams/impl/ludus-renderer/tests/test_cudaraster_api.py +++ b/integrations_v2/omnidreams/impl/ludus-renderer/tests/test_cudaraster_api.py @@ -2670,7 +2670,6 @@ def test_mixed_scale_overlap_respects_depth_order(harness: CudaRasterHarness) -> assert near_depth < far_depth - def test_timestamped_context_batches_distinct_timestamps_exactly() -> None: """A multi-frame call must match independent native submissions bit-for-bit.""" from ludus_renderer import ( From 006bce89caeba735778482cc76bd5802f8e92d1f Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Wed, 14 Oct 2026 11:01:11 -0700 Subject: [PATCH 12/16] offline packager docs clean up --- .../tools/offline_program_packager.md | 34 +---- tools/package-as-offline-exe/README.md | 141 +----------------- 2 files changed, 7 insertions(+), 168 deletions(-) diff --git a/docs/source/documentation/tools/offline_program_packager.md b/docs/source/documentation/tools/offline_program_packager.md index be527f957..9fb7ba582 100644 --- a/docs/source/documentation/tools/offline_program_packager.md +++ b/docs/source/documentation/tools/offline_program_packager.md @@ -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` are written to the writable runtime cache. | +| +| `--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: diff --git a/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index 872bbefd0..79ce19a20 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -1,140 +1 @@ -# Package a FlashDreams application for offline use - -`package_as_offline_exe.py` prepares an installed FlashDreams v2 application -and creates a raw folder containing an executable, its runtime, and offline cache. -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/taxi-omnidreams-bundle \ - ::: flashdreams-run-v2 crazy-robotaxi-omnidreams-fast-perf \ - --mode native-window --fullscreen -``` - -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 -``` - -If `--output` is omitted, the default is -`artifacts/-bundle`. The output directory must not already -exist. - -The executable embeds Python bytecode, which is read directly without extraction. -`_internal/` contains the Python runtime, dependencies, and native libraries. -The standard-library archive is also read in place. `cache/` holds downloaded -model weights and prepared kernels. Preload may also produce sibling `artifacts/` -assets. The packager uses PyInstaller's directory mode and does not extract -runtime files to a temporary directory at launch or archive the cache. -Deploy the complete folder as raw files. -Pass `--unpack-dir` to change where the writable data cache is copied. - - -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. - ---working-dir PATH - Defaults to the output directory (which must not already exist, ensuring nothing extra is copied if this option is used). Create if missing and copy its contents into the bundle before preload. - ---noconsole - Build without a console window on Windows (ignored on Linux). - Console-free launchers write stdout and stderr to runtime.log in the writable runtime cache. - ---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] -|-- _internal/ -|-- cache/ -|-- artifacts/ # when preload produces application assets -|-- INSTALLER_OUTPUT.txt -|-- PREPARATION_ISSUES.txt # only when preload warnings are found -`-- README.md -``` - -The executable contains embedded Python bytecode. `_internal/` contains the -Python runtime, supporting source files, dependencies, native libraries, Ninja, -and packaged GPU assets. -Native extensions are compiled during packaging and reused at runtime. The -destination machine does not need CUDA Toolkit or Visual Studio Build Tools -for those extensions, but it still needs a compatible NVIDIA GPU and driver. -If a prepared extension is missing, rebuild the bundle on the packaging host. - -Notes on generated files: - -- `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. - -The launcher sets its working directory to the bundle directory when started, -so relative application paths resolve beside the executable. - -## Runtime cache behavior - -The packaged `cache/` directory remains read-only. On first launch of the executable, -the launcher copies it into a writable runtime cache: - -- Windows: `%USERPROFILE%\flashdreams\unpacked\\cache` -- Linux: `~/flashdreams/unpacked//cache` - -Pass `--unpack-dir PATH` to the packaged executable to use -`PATH//cache` instead. `PATH` must be absolute. For example, -`interactive-drive-omnidreams-fast-perf.exe --unpack-dir D:\FlashDreamsData`. -`FLASHDREAMS_RUNTIME_CACHE_DIR` remains available to set the exact cache -directory; the command-line option takes precedence. Python and runtime DLLs -load directly from `_internal/`. Prepared C++, Triton, and PhysX extensions load from -the bundled `cache/`, even when the writable cache is relocated. -On Windows, cache paths use the -extended-length path prefix so deep Hugging Face snapshots can be copied. - -Keep the executable with its sibling `_internal/`, `cache/`, and any -`artifacts/` assets. Pass `--unpack-dir` to move the writable data 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) \ No newline at end of file From ab73b5dc92e371957e31a4d15246477ea27a40b0 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Wed, 14 Oct 2026 23:41:52 -0700 Subject: [PATCH 13/16] minor optimizations around math & logic --- .../simulation/game_physics.py | 2 +- .../simulation/ground_snap.py | 105 +++++++++++------- .../simulation/map_traffic.py | 38 +++---- 3 files changed, 81 insertions(+), 64 deletions(-) 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 f955aebb8..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 @@ -404,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 @@ -541,6 +537,7 @@ def _headway_scale( 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: @@ -562,10 +559,9 @@ def _headway_scale( 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 min( max((best_clearance - desired_clearance) / _BRAKING_MARGIN_M, 0.0), 1.0 ) From b75d973d68ba9df77c7ac5a73f1802185df5f86d Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Thu, 15 Oct 2026 10:11:28 -0700 Subject: [PATCH 14/16] using fullscreen sets default presentation res to fullscreen size. --- apps/crazy_robotaxi/crazy_robotaxi/ui.py | 9 +++++++++ apps/crazy_robotaxi/tests/test_ui.py | 12 +++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) 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_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() From eff868e0f3052543a49269d3f191069135d9af2d Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Thu, 15 Oct 2026 16:20:02 -0700 Subject: [PATCH 15/16] change default temp logging dir. --- .../tools/offline_program_packager.md | 2 +- .../check_launcher_streams.py | 3 ++- .../package_as_offline_exe.py | 25 ++++++++++++------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/source/documentation/tools/offline_program_packager.md b/docs/source/documentation/tools/offline_program_packager.md index 9fb7ba582..95e850b8f 100644 --- a/docs/source/documentation/tools/offline_program_packager.md +++ b/docs/source/documentation/tools/offline_program_packager.md @@ -39,7 +39,7 @@ 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` are written to the writable runtime cache. | +| `--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. | diff --git a/tools/package-as-offline-exe/check_launcher_streams.py b/tools/package-as-offline-exe/check_launcher_streams.py index bc9119fd7..2562a7fd9 100644 --- a/tools/package-as-offline-exe/check_launcher_streams.py +++ b/tools/package-as-offline-exe/check_launcher_streams.py @@ -28,6 +28,7 @@ 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), @@ -43,7 +44,7 @@ logger.info("launcher logging works") sys.stdout.write("launcher output works\n") if missing_stdout or missing_stderr: - log = Path(directory) / "cache" / "runtime.log" + 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 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 59e55e6c3..d26071b89 100644 --- a/tools/package-as-offline-exe/package_as_offline_exe.py +++ b/tools/package-as-offline-exe/package_as_offline_exe.py @@ -412,6 +412,18 @@ def _launcher_source( 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": @@ -494,15 +506,6 @@ def filesystem_path(path): cache_path.mkdir(parents=True, exist_ok=True) os.environ[name] = str(cache_path) - if sys.stdout is None or sys.stderr is None: - runtime_log = (cache_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 - if getattr(sys, "frozen", False): # Prepared native code must load from the deployed bundle. for name in ( @@ -638,6 +641,10 @@ def _bundle_readme_source(slug: str) -> str: 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 | From d068528badde1cbd719d14f56e7617c59d935b92 Mon Sep 17 00:00:00 2001 From: Ariel Glasroth Date: Sat, 17 Oct 2026 14:31:31 -0700 Subject: [PATCH 16/16] fix formatting --- docs/source/documentation/tools/offline_program_packager.md | 2 +- tools/package-as-offline-exe/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/documentation/tools/offline_program_packager.md b/docs/source/documentation/tools/offline_program_packager.md index 95e850b8f..f94943ee7 100644 --- a/docs/source/documentation/tools/offline_program_packager.md +++ b/docs/source/documentation/tools/offline_program_packager.md @@ -40,7 +40,7 @@ validation. If `--output` is omitted, the destination is | `--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: diff --git a/tools/package-as-offline-exe/README.md b/tools/package-as-offline-exe/README.md index 79ce19a20..31ba2bebf 100644 --- a/tools/package-as-offline-exe/README.md +++ b/tools/package-as-offline-exe/README.md @@ -1 +1 @@ -[Refer To Website For Documentation](https://flashdreams.nvidia.com/documentation/tools/offline_program_packager.html) \ No newline at end of file +[Refer To Website For Documentation](https://flashdreams.nvidia.com/documentation/tools/offline_program_packager.html)