Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/crazy_robotaxi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions apps/crazy_robotaxi/crazy_robotaxi/live_edit/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.

Expand All @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions apps/crazy_robotaxi/crazy_robotaxi/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions apps/crazy_robotaxi/tests/test_live_edit_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 11 additions & 1 deletion apps/crazy_robotaxi/tests/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ()
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -117,62 +119,85 @@ 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)
rot = _euler_to_rotation(state.yaw_rad, state.pitch_rad, state.roll_rad)
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))
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading