| name | ai-skybox-development |
| description | 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. |
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:
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:
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:
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.