# AI Skybox Development

> Guides development on the bevy_ai_skybox crate. Use when changing skybox generation, GPU equirect-to-cubemap conversion, GPU readback persistence, Bevy render-world wiring, examples, docs, or tests in this repository.

- Skill: `elodin-sys/ai-skybox-development` (Agent Skill)
- Install (CLI): `npx skillmds@latest add elodin-sys/ai-skybox-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/elodin-sys/ai-skybox-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: elodin-sys (https://skillmd.com/u/elodin-sys)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/elodin-sys/ai-skybox-development

---


# AI Skybox Development

## Core Invariants

- Treat GPU conversion as the baseline. Do not reintroduce `gpu-convert`, `ConvertBackend`, CPU equirect-to-cubemap sampling, or CPU fallback paths.
- Preserve the public message API unless explicitly asked: `GenerateSkybox`, `SetActiveSkybox`, `SkyboxReady`, `SkyboxFailed`.
- Preserve the manifest schema and asset naming unless explicitly asked: `*.equirect.png`, `*.cubemap.png`, `SkyboxManifest`, `ManifestEntry`.
- PNG encoding on CPU is acceptable; cubemap resampling is not. The persisted cubemap PNG should come from GPU-read cubemap bytes.
- Be careful with API keys in logs or terminal selections. Do not reuse exposed keys; tell the user to rotate any key that appears in chat.

## GPU Conversion And Readback

- Queue conversion from the main world through `PendingGpuConversions`; dispatch compute in `EquirectToCubemapNode`.
- After compute dispatch, copy the cube texture to a staging buffer with `COPY_DST | MAP_READ`.
- Never call `map_async` before the command encoder that writes the buffer has been submitted. Queue buffers in the render node, then map them from a later render schedule stage such as `RenderSystems::Cleanup`.
- `copy_texture_to_buffer` requires 256-byte row alignment. Use `RenderDevice::align_copy_bytes_per_row`, size the staging buffer with padded rows, and compact padding before sending bytes through `ReadbackChannel`.
- Keep the live `Skybox` handle as the source of truth. When persistence completes, reuse the existing cube handle rather than loading the just-written PNG back through `AssetServer`.
- If `RenderApp` is absent, fail clearly. This crate assumes Bevy rendering is available, including headless render-world setups.

## Headless Bevy Patterns

For GPU code, `MinimalPlugins + AssetPlugin + ImagePlugin` is not enough. Use the full render stack without opening a window:

```rust
DefaultPlugins
    .set(WindowPlugin {
        primary_window: None,
        exit_condition: ExitCondition::DontExit,
        close_when_requested: false,
        ..default()
    })
    .disable::<WinitPlugin>()
```

For examples that need to run until async render work completes, prefer `ScheduleRunnerPlugin::run_loop(...)` plus `AppExit` messages over a manual `app.update()` loop.

## Testing Requirements

Compile checks are not enough for GPU readback changes.

Run, at minimum:

```sh
cargo fmt
cargo check --all-targets
cargo test --lib --tests
cargo run --release --example gpu_readback_smoke
```

`gpu_readback_smoke` is the local non-Blockade test for the render graph, compute dispatch, staging-buffer copy, map timing, row-padding compaction, and `ReadbackChannel` delivery.

For end-to-end API validation, use:

```sh
BLOCKADE_API_KEY=... cargo run --release --example live_pipeline -- "short prompt" --res=1k
```

Only run live API tests when the user explicitly approves using their Blockade quota/key.

## Documentation Expectations

When conversion or persistence behavior changes, update:

- `ARCHITECTURE.md` for render graph, state machine, and persistence strategy.
- `README.md` for user-facing commands and example list.
- `CHANGELOG.md` for breaking public API or workflow changes.

Docs should not describe CPU conversion as available. Mention GPU readback row padding and delayed mapping if relevant to the change.

## Common Failure Modes

- `Buffer ... is still mapped` during `Queue::submit`: `map_async` happened before GPU submission. Move mapping later.
- `Bytes per row does not respect COPY_BYTES_PER_ROW_ALIGNMENT`: staging buffer rows were not padded to 256 bytes.
- Headless example panics about missing `RenderDevice`: the app did not use the full render stack or ran without the schedule runner pattern.
- Generated skybox displays but no `SkyboxReady`: inspect `ReadbackChannel` draining and the `GpuConverting -> Persisting` transition.

