Improve editor workflows and thumbnail resilience

This commit is contained in:
Rbanh 2026-07-10 00:48:25 -04:00
parent b5e561904c
commit cc1fa6ea3b
9 changed files with 465 additions and 135 deletions

View File

@ -41,34 +41,22 @@ jobs:
run: rustup show run: rustup show
- name: Check formatting - name: Check formatting
run: cargo fmt --check run: cargo fmt --all --check
- name: Check workspace - name: Check workspace
run: cargo check --workspace run: cargo check --workspace --all-targets
- name: Clippy - name: Clippy workspace
run: cargo clippy --workspace run: cargo clippy --workspace --all-targets -- -D warnings
- name: Clippy foundation crates - name: Test workspace
run: cargo clippy -p game -p shared -- -D warnings run: cargo test --workspace
- name: Test foundation helpers
run: cargo test -p game --lib
- name: Test shared hydration and actor validation
run: cargo test -p shared
- name: Test sim crate
run: cargo test -p sim
- name: Validate level scenes - name: Validate level scenes
run: cargo validate-levels run: cargo validate-levels
- name: Test scene schema crate - name: Clippy launch feature matrix
run: cargo test -p scene run: cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings
- name: Check editor (dev)
run: cargo check -p editor --features dev
- name: Build binaries - name: Build binaries
run: | run: |

244
.vscode/launch.json vendored
View File

@ -4,48 +4,222 @@
{ {
"type": "lldb", "type": "lldb",
"request": "launch", "request": "launch",
"name": "Debug editor", "name": "Editor: Debug",
"preLaunchTask": "build editor (dev fast-link)",
"program": "${workspaceFolder}/target/debug/editor",
"args": [],
"env": {
"BEVY_FPS_HDR": "0",
"WGPU_VALIDATION": "0",
"CARGO_TARGET_DIR": "${workspaceFolder}/target",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
},
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug game",
"preLaunchTask": "build game (dev fast-link)",
"program": "${workspaceFolder}/target/debug/game",
"args": [],
"env": {
"BEVY_FPS_HDR": "0",
"WGPU_VALIDATION": "0",
"CARGO_TARGET_DIR": "${workspaceFolder}/target",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
},
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Run editor (release)",
"cargo": { "cargo": {
"args": ["build", "--release", "-p", "editor"], "args": ["build", "-p", "editor", "--bin", "editor", "--features", "dev"],
"filter": { "name": "editor", "kind": "bin" } "filter": { "name": "editor", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
}, },
"args": [], "args": [],
"env": { "env": {
"RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "0",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
},
"cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
},
{
"type": "lldb",
"request": "launch",
"name": "Editor: GPU validation",
"cargo": {
"args": ["build", "-p", "editor", "--bin", "editor", "--features", "dev"],
"filter": { "name": "editor", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
},
"args": [],
"env": {
"RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "1",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
},
"cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
},
{
"type": "lldb",
"request": "launch",
"name": "Editor: Hot reload",
"cargo": {
"args": [
"build",
"-p",
"editor",
"--bin",
"editor",
"--features",
"dev,hot-reload"
],
"filter": { "name": "editor", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
},
"args": [],
"env": {
"RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "0",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
},
"cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
},
{
"type": "lldb",
"request": "launch",
"name": "Editor: SDR fallback",
"cargo": {
"args": ["build", "-p", "editor", "--bin", "editor", "--features", "dev"],
"filter": { "name": "editor", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
},
"args": [],
"env": {
"RUST_BACKTRACE": "1",
"BEVY_FPS_HDR": "0", "BEVY_FPS_HDR": "0",
"WGPU_VALIDATION": "0", "WGPU_VALIDATION": "0",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/release/deps:${env:LD_LIBRARY_PATH}" "LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
}, },
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
},
{
"type": "lldb",
"request": "launch",
"name": "Editor: Release",
"cargo": {
"args": ["build", "--release", "-p", "editor", "--bin", "editor"],
"filter": { "name": "editor", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
},
"args": [],
"env": { "RUST_BACKTRACE": "1", "WGPU_VALIDATION": "0" },
"cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
},
{
"type": "lldb",
"request": "launch",
"name": "Game: Debug",
"cargo": {
"args": ["build", "-p", "game", "--bin", "game", "--features", "dev"],
"filter": { "name": "game", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
},
"args": [],
"env": {
"RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "0",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
},
"cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
},
{
"type": "lldb",
"request": "launch",
"name": "Game: GPU validation",
"cargo": {
"args": ["build", "-p", "game", "--bin", "game", "--features", "dev"],
"filter": { "name": "game", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
},
"args": [],
"env": {
"RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "1",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
},
"cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
},
{
"type": "lldb",
"request": "launch",
"name": "Game: Hot reload",
"cargo": {
"args": [
"build",
"-p",
"game",
"--bin",
"game",
"--features",
"dev,hot-reload"
],
"filter": { "name": "game", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
},
"args": [],
"env": {
"RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "0",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
},
"cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
},
{
"type": "lldb",
"request": "launch",
"name": "Game: SDR fallback",
"cargo": {
"args": ["build", "-p", "game", "--bin", "game", "--features", "dev"],
"filter": { "name": "game", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
},
"args": [],
"env": {
"RUST_BACKTRACE": "1",
"BEVY_FPS_HDR": "0",
"WGPU_VALIDATION": "0",
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug/deps:${env:LD_LIBRARY_PATH}"
},
"cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
},
{
"type": "lldb",
"request": "launch",
"name": "Game: Release",
"cargo": {
"args": ["build", "--release", "-p", "game", "--bin", "game"],
"filter": { "name": "game", "kind": "bin" },
"env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" },
"problemMatcher": "$rustc"
},
"args": [],
"env": { "RUST_BACKTRACE": "1", "WGPU_VALIDATION": "0" },
"cwd": "${workspaceFolder}",
"terminal": "integrated",
"sourceLanguages": ["rust"],
"gracefulShutdown": "SIGINT"
} }
] ]
} }

207
.vscode/tasks.json vendored
View File

@ -2,42 +2,61 @@
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"label": "cargo fmt (check)", "label": "cargo fmt (workspace check)",
"type": "cargo", "type": "cargo",
"command": "fmt", "command": "fmt",
"args": ["--check"], "args": ["--all", "--check"],
"problemMatcher": [], "problemMatcher": [],
"group": "build" "group": "build"
}, },
{ {
"label": "cargo check (workspace)", "label": "cargo check (workspace all targets)",
"type": "cargo", "type": "cargo",
"command": "check", "command": "check",
"args": ["--workspace"], "args": ["--workspace", "--all-targets"],
"problemMatcher": ["$rustc"], "problemMatcher": ["$rustc"],
"group": "build" "group": "build"
}, },
{ {
"label": "cargo clippy (workspace)", "label": "cargo clippy (workspace strict)",
"type": "cargo", "type": "cargo",
"command": "clippy", "command": "clippy",
"args": ["--workspace"], "args": ["--workspace", "--all-targets", "--", "-D", "warnings"],
"problemMatcher": ["$rustc"], "problemMatcher": ["$rustc"],
"group": "build" "group": "build"
}, },
{ {
"label": "cargo clippy (foundation strict)", "label": "cargo test (workspace)",
"type": "cargo",
"command": "clippy",
"args": ["-p", "game", "-p", "shared", "--", "-D", "warnings"],
"problemMatcher": ["$rustc"],
"group": "build"
},
{
"label": "cargo test (foundation)",
"type": "cargo", "type": "cargo",
"command": "test", "command": "test",
"args": ["-p", "game", "--lib"], "args": ["--workspace"],
"problemMatcher": ["$rustc"],
"group": "test"
},
{
"label": "cargo clippy (launch feature matrix)",
"type": "cargo",
"command": "clippy",
"args": [
"-p",
"editor",
"-p",
"game",
"--bins",
"--features",
"dev,hot-reload",
"--",
"-D",
"warnings"
],
"problemMatcher": ["$rustc"],
"group": "build"
},
{
"label": "validate level scenes",
"type": "cargo",
"command": "validate-levels",
"args": [],
"problemMatcher": ["$rustc"], "problemMatcher": ["$rustc"],
"group": "test" "group": "test"
}, },
@ -45,7 +64,7 @@
"label": "build game (debug)", "label": "build game (debug)",
"type": "cargo", "type": "cargo",
"command": "build", "command": "build",
"args": ["-p", "game"], "args": ["-p", "game", "--bin", "game"],
"problemMatcher": ["$rustc"], "problemMatcher": ["$rustc"],
"group": "build" "group": "build"
}, },
@ -53,47 +72,50 @@
"label": "build editor (debug)", "label": "build editor (debug)",
"type": "cargo", "type": "cargo",
"command": "build", "command": "build",
"args": ["-p", "editor"], "args": ["-p", "editor", "--bin", "editor"],
"problemMatcher": ["$rustc"], "problemMatcher": ["$rustc"],
"group": { "kind": "build", "isDefault": true } "group": "build"
}, },
{ {
"label": "build editor (dev fast-link)", "label": "build editor (dev fast-link)",
"type": "cargo", "type": "cargo",
"command": "build", "command": "build",
"args": ["-p", "editor", "--features", "dev"], "args": ["-p", "editor", "--bin", "editor", "--features", "dev"],
"options": { "options": {
"env": { "cwd": "${workspaceFolder}",
"CARGO_TARGET_DIR": "${workspaceFolder}/target" "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" }
}
}, },
"problemMatcher": ["$rustc"], "problemMatcher": ["$rustc"],
"group": "build" "group": { "kind": "build", "isDefault": true }
}, },
{ {
"label": "build game (dev fast-link)", "label": "build game (dev fast-link)",
"type": "cargo", "type": "cargo",
"command": "build", "command": "build",
"args": ["-p", "game", "--features", "dev"], "args": ["-p", "game", "--bin", "game", "--features", "dev"],
"options": { "options": {
"env": { "cwd": "${workspaceFolder}",
"CARGO_TARGET_DIR": "${workspaceFolder}/target" "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" }
}
}, },
"problemMatcher": ["$rustc"], "problemMatcher": ["$rustc"],
"group": "build" "group": "build"
}, },
{ {
"label": "clean build editor (dev)", "label": "clean dev-link crates",
"type": "shell", "type": "cargo",
"command": "cargo clean -p editor && cargo build -p editor --features dev", "command": "clean",
"args": ["-p", "editor", "-p", "game", "-p", "game_hot", "-p", "shared"],
"options": { "options": {
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"env": { "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" }
"CARGO_TARGET_DIR": "${workspaceFolder}/target"
}
}, },
"problemMatcher": ["$rustc"], "problemMatcher": []
},
{
"label": "clean build editor (dev)",
"dependsOrder": "sequence",
"dependsOn": ["clean dev-link crates", "build editor (dev fast-link)"],
"problemMatcher": [],
"group": "build" "group": "build"
}, },
{ {
@ -102,9 +124,7 @@
"command": "cargo clean-target", "command": "cargo clean-target",
"options": { "options": {
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"env": { "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" }
"CARGO_TARGET_DIR": "${workspaceFolder}/target"
}
}, },
"problemMatcher": [], "problemMatcher": [],
"group": "build" "group": "build"
@ -115,9 +135,7 @@
"command": "cargo clean-target --apply", "command": "cargo clean-target --apply",
"options": { "options": {
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"env": { "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" }
"CARGO_TARGET_DIR": "${workspaceFolder}/target"
}
}, },
"problemMatcher": [], "problemMatcher": [],
"group": "build" "group": "build"
@ -128,9 +146,7 @@
"command": "cargo clean-target --include-artifacts --days 3 --apply", "command": "cargo clean-target --include-artifacts --days 3 --apply",
"options": { "options": {
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"env": { "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" }
"CARGO_TARGET_DIR": "${workspaceFolder}/target"
}
}, },
"problemMatcher": [], "problemMatcher": [],
"group": "build" "group": "build"
@ -139,10 +155,11 @@
"label": "run editor (dev fast-link)", "label": "run editor (dev fast-link)",
"type": "cargo", "type": "cargo",
"command": "run", "command": "run",
"args": ["-p", "editor", "--features", "dev"], "args": ["-p", "editor", "--bin", "editor", "--features", "dev"],
"options": { "options": {
"cwd": "${workspaceFolder}",
"env": { "env": {
"BEVY_FPS_HDR": "0", "RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "0", "WGPU_VALIDATION": "0",
"CARGO_TARGET_DIR": "${workspaceFolder}/target" "CARGO_TARGET_DIR": "${workspaceFolder}/target"
} }
@ -154,9 +171,83 @@
"label": "run game (dev fast-link)", "label": "run game (dev fast-link)",
"type": "cargo", "type": "cargo",
"command": "run", "command": "run",
"args": ["-p", "game", "--features", "dev"], "args": ["-p", "game", "--bin", "game", "--features", "dev"],
"options": { "options": {
"cwd": "${workspaceFolder}",
"env": { "env": {
"RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "0",
"CARGO_TARGET_DIR": "${workspaceFolder}/target"
}
},
"problemMatcher": ["$rustc"],
"group": "build"
},
{
"label": "run editor (hot reload)",
"type": "cargo",
"command": "run",
"args": [
"-p",
"editor",
"--bin",
"editor",
"--features",
"dev,hot-reload"
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "0",
"CARGO_TARGET_DIR": "${workspaceFolder}/target"
}
},
"problemMatcher": ["$rustc"],
"group": "build"
},
{
"label": "run game (hot reload)",
"type": "cargo",
"command": "run",
"args": ["-p", "game", "--bin", "game", "--features", "dev,hot-reload"],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"RUST_BACKTRACE": "1",
"WGPU_VALIDATION": "0",
"CARGO_TARGET_DIR": "${workspaceFolder}/target"
}
},
"problemMatcher": ["$rustc"],
"group": "build"
},
{
"label": "run editor (SDR fallback)",
"type": "cargo",
"command": "run",
"args": ["-p", "editor", "--bin", "editor", "--features", "dev"],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"RUST_BACKTRACE": "1",
"BEVY_FPS_HDR": "0",
"WGPU_VALIDATION": "0",
"CARGO_TARGET_DIR": "${workspaceFolder}/target"
}
},
"problemMatcher": ["$rustc"],
"group": "build"
},
{
"label": "run game (SDR fallback)",
"type": "cargo",
"command": "run",
"args": ["-p", "game", "--bin", "game", "--features", "dev"],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"RUST_BACKTRACE": "1",
"BEVY_FPS_HDR": "0", "BEVY_FPS_HDR": "0",
"WGPU_VALIDATION": "0", "WGPU_VALIDATION": "0",
"CARGO_TARGET_DIR": "${workspaceFolder}/target" "CARGO_TARGET_DIR": "${workspaceFolder}/target"
@ -171,28 +262,28 @@
"command": "cargo watch -w crates/game_hot -w crates/sim -x \"build -p game_hot --features dylib\"", "command": "cargo watch -w crates/game_hot -w crates/sim -x \"build -p game_hot --features dylib\"",
"options": { "options": {
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"env": { "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" }
"CARGO_TARGET_DIR": "${workspaceFolder}/target"
}
}, },
"problemMatcher": ["$rustc"], "problemMatcher": ["$rustc"],
"group": "build", "group": "build",
"isBackground": true "isBackground": true,
"detail": "Requires cargo-watch: cargo install cargo-watch --locked"
}, },
{ {
"label": "verify foundation", "label": "verify workspace",
"dependsOrder": "sequence", "dependsOrder": "sequence",
"dependsOn": [ "dependsOn": [
"cargo fmt (check)", "cargo fmt (workspace check)",
"cargo check (workspace)", "cargo check (workspace all targets)",
"cargo clippy (workspace)", "cargo clippy (workspace strict)",
"cargo clippy (foundation strict)", "cargo test (workspace)",
"cargo test (foundation)", "cargo clippy (launch feature matrix)",
"validate level scenes",
"build game (debug)", "build game (debug)",
"build editor (debug)" "build editor (debug)"
], ],
"problemMatcher": [], "problemMatcher": [],
"group": { "kind": "build", "isDefault": true } "group": "build"
} }
] ]
} }

View File

@ -21,12 +21,12 @@ native Bevy scene save/load, and BRP support for external tooling.
```bash ```bash
# Check everything # Check everything
cargo fmt --check cargo fmt --all --check
cargo check --workspace cargo check --workspace --all-targets
cargo clippy --workspace cargo clippy --workspace --all-targets -- -D warnings
cargo clippy -p game -p shared -p protocol -p sim -- -D warnings cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings
cargo test -p game --lib cargo test --workspace
cargo test -p sim cargo validate-levels
# Runtime game # Runtime game
cargo run -p game cargo run -p game
@ -53,7 +53,7 @@ cargo watch -w crates/game_hot -w crates/sim -x "build -p game_hot --features dy
cargo run -p editor --features dev,hot-reload cargo run -p editor --features dev,hot-reload
``` ```
Or use the VS Code task **watch game_hot (hot reload)** alongside **run editor (dev fast-link)** (enable the `hot-reload` feature on the editor run task when using dylib iteration). Or use the VS Code task **watch game_hot (hot reload)** alongside **run editor (hot reload)**, or choose **Editor: Hot reload** in Run and Debug. The watcher task requires `cargo-watch` (`cargo install cargo-watch --locked`).
| Input | Action | | Input | Action |
|-------|--------| |-------|--------|
@ -90,8 +90,8 @@ deep-stale variants.
- The native game/editor windows force an opaque Wayland surface and opaque camera clears to avoid compositor alpha issues on mixed HDR/SDR desktops. - The native game/editor windows force an opaque Wayland surface and opaque camera clears to avoid compositor alpha issues on mixed HDR/SDR desktops.
- If the window maps but appears transparent on Hyprland or another Wayland compositor, launch with `BEVY_FPS_HDR=0` to force the SDR camera path while debugging monitor/compositor behavior. - If the window maps but appears transparent on Hyprland or another Wayland compositor, launch with `BEVY_FPS_HDR=0` to force the SDR camera path while debugging monitor/compositor behavior.
- Bevy 0.19 removed the prior local `bevy_render` swapchain-timeout patch; launch troubleshooting should start from current wgpu/driver/compositor logs. - Bevy 0.19 removed the prior local `bevy_render` swapchain-timeout patch; launch troubleshooting should start from current wgpu/driver/compositor logs.
- Debug launch configs and run tasks set `WGPU_VALIDATION=0` to quiet the known wgpu/Vulkan validation-layer warning `VUID-StandaloneSpirv-MemorySemantics-10871`. This only disables the Vulkan validation layer for those launches; Rust panics and application errors still surface normally. - Normal Debug and run configurations preserve project HDR and set `WGPU_VALIDATION=0` to suppress known Bevy/Solari Vulkan memory-model VUID noise on this stack. Use **GPU validation** when actively debugging renderer work; it forces `WGPU_VALIDATION=1` and may report those known upstream/driver messages. Use **SDR fallback** for compositor/HDR mapping failures; it additionally sets `BEVY_FPS_HDR=0`.
- If **CodeLLDB / mold** fails with hundreds of `undefined symbol` linker errors, the incremental `target/` cache is stale. Run the VS Code task **clean build editor (dev)** or `cargo clean -p editor && cargo build -p editor --features dev`, then launch **Debug editor** again. Use `cargo run -p editor --features dev` from the terminal if you need `libbevy_dylib` on `LD_LIBRARY_PATH` automatically. - If **CodeLLDB / mold** fails with hundreds of `undefined symbol` linker errors, the incremental `target/` cache is stale. Run the VS Code task **clean build editor (dev)** or `cargo clean -p editor -p game -p game_hot -p shared && cargo build -p editor --bin editor --features dev`, then launch **Editor: Debug** again. Use `cargo run -p editor --features dev` from the terminal if you need `libbevy_dylib` on `LD_LIBRARY_PATH` automatically.
## Editor Controls ## Editor Controls
@ -238,8 +238,8 @@ The `.vscode/` folder is preconfigured:
- `extensions.json` recommends rust-analyzer, Even Better TOML, CodeLLDB, crates, and Error Lens. - `extensions.json` recommends rust-analyzer, Even Better TOML, CodeLLDB, crates, and Error Lens.
- `settings.json` runs `clippy` on save, enables proc-macro/build-script support, formats on save, - `settings.json` runs `clippy` on save, enables proc-macro/build-script support, formats on save,
and excludes `target/` from search/watch. and excludes `target/` from search/watch.
- `tasks.json` provides workspace check/clippy plus game/editor build/run tasks. - `tasks.json` mirrors the full workspace formatting, all-target check, strict Clippy, test, level-validation, build/run, hot-reload, and target-cleanup workflows. **build editor (dev fast-link)** is the default build task.
- `launch.json` provides CodeLLDB launch configs for the editor and game. - `launch.json` uses CodeLLDB Cargo artifact filtering for editor/game Debug, GPU-validation, hot-reload, SDR fallback, and Release configurations. Normal launches keep HDR enabled while quieting known Vulkan validation noise; dynamic-link launches set the required `LD_LIBRARY_PATH` automatically.
- `.github/workflows/ci.yml` mirrors local formatting, check, clippy, test, and binary build verification. - `.github/workflows/ci.yml` mirrors local formatting, check, clippy, test, and binary build verification.
## Architecture Decisions ## Architecture Decisions

View File

@ -6,7 +6,7 @@ use bevy::prelude::*;
use bevy_egui::{egui, EguiPrimaryContextPass, EguiTextureHandle, EguiUserTextures}; use bevy_egui::{egui, EguiPrimaryContextPass, EguiTextureHandle, EguiUserTextures};
use egui_phosphor_icons::icons; use egui_phosphor_icons::icons;
use super::sources::gltf::gltf_base_color_texture_path; use super::sources::gltf::{gltf_base_color_texture_path, validate_gltf_dependencies};
use super::studio::{model_file_exists, ThumbnailStudio}; use super::studio::{model_file_exists, ThumbnailStudio};
use super::ThumbnailJobSource; use super::ThumbnailJobSource;
use crate::assets::{ use crate::assets::{
@ -100,13 +100,12 @@ impl AssetThumbnailCache {
return; return;
} }
if let Some(texture_path) = gltf_base_color_texture_path(&model_path) { if !self.model_source_ready(&key, &model_path) {
self.request_texture(key, texture_path, asset_server);
return; return;
} }
if !model_file_exists(&model_path) { if let Some(texture_path) = gltf_base_color_texture_path(&model_path) {
self.mark_studio_failed(&key, "file not found", false); self.request_texture(key, texture_path, asset_server);
return; return;
} }
@ -130,8 +129,7 @@ impl AssetThumbnailCache {
{ {
return; return;
} }
if !model_file_exists(&model_path) { if !self.model_source_ready(&key, &model_path) {
self.mark_studio_failed(&key, "file not found", false);
return; return;
} }
if studio.enqueue_source( if studio.enqueue_source(
@ -160,8 +158,7 @@ impl AssetThumbnailCache {
{ {
return; return;
} }
if !model_file_exists(&model_path) { if !self.model_source_ready(&key, &model_path) {
self.mark_studio_failed(&key, "file not found", false);
return; return;
} }
if studio.enqueue_source( if studio.enqueue_source(
@ -206,6 +203,18 @@ impl AssetThumbnailCache {
} }
} }
fn model_source_ready(&mut self, key: &str, model_path: &str) -> bool {
if !model_file_exists(model_path) {
self.mark_studio_failed(key, "file not found", false);
return false;
}
if let Err(reason) = validate_gltf_dependencies(model_path) {
self.mark_studio_failed(key, &reason, false);
return false;
}
true
}
pub(crate) fn complete_studio_thumbnail( pub(crate) fn complete_studio_thumbnail(
&mut self, &mut self,
key: &str, key: &str,

View File

@ -29,6 +29,41 @@ pub fn gltf_base_color_texture_path(model_asset_path: &str) -> Option<String> {
None None
} }
/// Verifies local external buffers and images before Bevy starts an async glTF load.
pub fn validate_gltf_dependencies(model_asset_path: &str) -> Result<(), String> {
let model_path = Path::new(model_asset_path);
if !is_gltf_path(model_path) {
return Ok(());
}
let gltf = gltf::Gltf::open(model_path).map_err(|error| format!("invalid glTF: {error}"))?;
let mut uris = Vec::new();
for buffer in gltf.buffers() {
if let gltf::buffer::Source::Uri(uri) = buffer.source() {
uris.push(uri);
}
}
for image in gltf.images() {
if let gltf::image::Source::Uri { uri, .. } = image.source() {
uris.push(uri);
}
}
let model_dir = model_path.parent().unwrap_or_else(|| Path::new(""));
for uri in uris {
if uri.starts_with("data:") || uri.contains("://") {
continue;
}
let dependency = model_dir.join(uri);
if !dependency.is_file() {
let display = normalize_asset_path(&dependency)
.unwrap_or_else(|| dependency.to_string_lossy().replace('\\', "/"));
return Err(format!("missing dependency: {display}"));
}
}
Ok(())
}
pub struct GltfThumbnailSource; pub struct GltfThumbnailSource;
fn is_gltf_path(path: &Path) -> bool { fn is_gltf_path(path: &Path) -> bool {
@ -61,7 +96,9 @@ fn normalize_asset_path(path: &Path) -> Option<String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[test] #[test]
fn normalize_asset_path_keeps_assets_prefix() { fn normalize_asset_path_keeps_assets_prefix() {
@ -79,4 +116,25 @@ mod tests {
Some("assets/models/textures/chair_albedo.png".into()) Some("assets/models/textures/chair_albedo.png".into())
); );
} }
#[test]
fn dependency_validation_reports_missing_external_buffer() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let directory = std::env::temp_dir().join(format!("blacksite-gltf-{nonce}"));
fs::create_dir_all(&directory).unwrap();
let model = directory.join("missing-buffer.gltf");
fs::write(
&model,
r#"{"asset":{"version":"2.0"},"buffers":[{"uri":"missing.bin","byteLength":4}]}"#,
)
.unwrap();
let error = validate_gltf_dependencies(model.to_str().unwrap()).unwrap_err();
assert!(error.contains("missing.bin"));
fs::remove_dir_all(directory).unwrap();
}
} }

View File

@ -315,6 +315,9 @@ fn process_thumbnail_studio(
ThumbnailStudioLayer, ThumbnailStudioLayer,
RenderLayers::layer(THUMBNAIL_LAYER), RenderLayers::layer(THUMBNAIL_LAYER),
Transform::default(), Transform::default(),
Visibility::Visible,
InheritedVisibility::default(),
ViewVisibility::default(),
)) ))
.id(); .id();

View File

@ -1,5 +1,12 @@
//! Runtime game crate: FPS player, rendering, and starter world. //! Runtime game crate: FPS player, rendering, and starter world.
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
#![cfg_attr(
feature = "hot-reload",
allow(
clippy::too_many_arguments,
reason = "hot-lib-reloader wrappers preserve Bevy system signatures and macro spans"
)
)]
pub mod editor_ext; pub mod editor_ext;
pub mod launch; pub mod launch;
@ -62,8 +69,8 @@ mod systems {
mod hot { mod hot {
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::input::mouse::AccumulatedMouseMotion; use bevy::input::mouse::AccumulatedMouseMotion;
use bevy::light::atmosphere::ScatteringMedium;
use bevy::light::CascadeShadowConfig; use bevy::light::CascadeShadowConfig;
use bevy::pbr::ScatteringMedium;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::time::Fixed; use bevy::time::Fixed;
use bevy::window::{CursorOptions, PrimaryWindow}; use bevy::window::{CursorOptions, PrimaryWindow};

View File

@ -41,7 +41,7 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
- **Dedicated egui `Camera2d`** at full window — never attach `PrimaryEguiContext` to a viewport-cropped 3D camera (NaN layout panic). - **Dedicated egui `Camera2d`** at full window — never attach `PrimaryEguiContext` to a viewport-cropped 3D camera (NaN layout panic).
- **Unified viewport** uses one render-to-texture target for both the editor fly camera and the possessed player camera so HDR/atmosphere is not broken by sub-viewport cropping. - **Unified viewport** uses one render-to-texture target for both the editor fly camera and the possessed player camera so HDR/atmosphere is not broken by sub-viewport cropping.
- **PIE:** F8 possess/eject while sim runs; **F6** pauses/resumes simulation in Play; project settings drive shared rendering for the active viewport camera. - **PIE:** F8 possess/eject while sim runs; **F6** pauses/resumes simulation in Play; project settings drive shared rendering for the active viewport camera.
- **Asset browser** mirrors the on-disk `assets/` directory with project tree, breadcrumb, search/filter/sort, grid/list views, texture/model/material thumbnails, a details pane, and context-aware row/menu actions; narrow docks prioritize content, keep the root panel fixed, switch list view to a compact single-column layout, and hide tree/details panes when they would crowd the content area. The footer stays pinned while only the project tree, asset content, and details panels scroll. **Built-ins** holds spawnable primitives and lights. **Materials** folder scans `assets/materials/*.ron`, renders material thumbnails on a sphere using `MaterialDesc`, and exposes shader-schema-driven parameters/textures in the details editor; **Shaders** holds shader schema RON files. glTF/GLB/FBX rows can expand into a shelf of normalized embedded mesh, material, and texture subassets with independent generated thumbnails. Mesh subassets can be selected, dragged into the viewport, or placed from details/context menus; material subassets render source-material spheres; texture subassets can be applied to the selected actor. Model import settings are staged with **Apply** / **Revert**, asset context menus can regenerate thumbnails, material asset details edit shared `MaterialAsset` fields, and file asset deletion moves sources/generated artifacts into `assets/.trash/`. - **Asset browser** mirrors the on-disk `assets/` directory with project tree, breadcrumb, search/filter/sort, grid/list views, texture/model/material thumbnails, a details pane, and context-aware row/menu actions; narrow docks prioritize content, keep the root panel fixed, switch list view to a compact single-column layout, and hide tree/details panes when they would crowd the content area. The footer stays pinned while only the project tree, asset content, and details panels scroll. **Built-ins** holds spawnable primitives and lights. **Materials** folder scans `assets/materials/*.ron`, renders material thumbnails on a sphere using `MaterialDesc`, and exposes shader-schema-driven parameters/textures in the details editor; **Shaders** holds shader schema RON files. glTF/GLB/FBX rows can expand into a shelf of normalized embedded mesh, material, and texture subassets with independent generated thumbnails. Mesh subassets can be selected, dragged into the viewport, or placed from details/context menus; material subassets render source-material spheres; texture subassets can be applied to the selected actor. Model import settings are staged with **Apply** / **Revert**, asset context menus can regenerate thumbnails, material asset details edit shared `MaterialAsset` fields, and file asset deletion moves sources/generated artifacts into `assets/.trash/`. Before thumbnail loading, glTF sources preflight local external buffers/images and show a stable non-retryable failure state when a dependency is missing instead of repeatedly invoking the asset loader.
- **Static mesh renderer** — model drag/drop defaults to `ActorKind::StaticMesh + StaticMeshRenderer` using normalized artifacts under `assets/meshes/generated/`. Renderer slots store imported `EditorAssetRef` mesh/material references, not source FBX/glTF paths. `SceneInstance` placement keeps the legacy `ImportedModel + ModelRef` path for full scene playback. - **Static mesh renderer** — model drag/drop defaults to `ActorKind::StaticMesh + StaticMeshRenderer` using normalized artifacts under `assets/meshes/generated/`. Renderer slots store imported `EditorAssetRef` mesh/material references, not source FBX/glTF paths. `SceneInstance` placement keeps the legacy `ImportedModel + ModelRef` path for full scene playback.
- **Brush authoring**`ActorKind::Brush + BrushDesc` stores persisted convex blockout faces, validates authored geometry in the inspector and Window → Brush Diagnostics, and hydrates active valid brushes into generated mesh children. See [brushes.md](brushes.md). - **Brush authoring**`ActorKind::Brush + BrushDesc` stores persisted convex blockout faces, validates authored geometry in the inspector and Window → Brush Diagnostics, and hydrates active valid brushes into generated mesh children. See [brushes.md](brushes.md).
- **Draw Brush**`B`, toolbar pencil, or command `brush.draw` enters a floor-polygon draw mode. LMB places snapped points, Backspace removes the last point, Enter locks the outline for height editing, mouse up/down adjusts height, and Enter/LMB creates additive prism brushes through history. Esc/right-click cancels. Simple concave outlines decompose into convex brush parts; self-intersections remain blocked. - **Draw Brush**`B`, toolbar pencil, or command `brush.draw` enters a floor-polygon draw mode. LMB places snapped points, Backspace removes the last point, Enter locks the outline for height editing, mouse up/down adjusts height, and Enter/LMB creates additive prism brushes through history. Esc/right-click cancels. Simple concave outlines decompose into convex brush parts; self-intersections remain blocked.