diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54d188f..93bb078 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,34 +41,22 @@ jobs: run: rustup show - name: Check formatting - run: cargo fmt --check + run: cargo fmt --all --check - name: Check workspace - run: cargo check --workspace + run: cargo check --workspace --all-targets - - name: Clippy - run: cargo clippy --workspace + - name: Clippy workspace + run: cargo clippy --workspace --all-targets -- -D warnings - - name: Clippy foundation crates - run: cargo clippy -p game -p shared -- -D warnings - - - 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: Test workspace + run: cargo test --workspace - name: Validate level scenes run: cargo validate-levels - - name: Test scene schema crate - run: cargo test -p scene - - - name: Check editor (dev) - run: cargo check -p editor --features dev + - name: Clippy launch feature matrix + run: cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings - name: Build binaries run: | diff --git a/.vscode/launch.json b/.vscode/launch.json index 3ba308b..ac2b63f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,48 +4,222 @@ { "type": "lldb", "request": "launch", - "name": "Debug editor", - "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)", + "name": "Editor: Debug", "cargo": { - "args": ["build", "--release", "-p", "editor"], - "filter": { "name": "editor", "kind": "bin" } + "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": "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", "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" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7610ed4..3692069 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,42 +2,61 @@ "version": "2.0.0", "tasks": [ { - "label": "cargo fmt (check)", + "label": "cargo fmt (workspace check)", "type": "cargo", "command": "fmt", - "args": ["--check"], + "args": ["--all", "--check"], "problemMatcher": [], "group": "build" }, { - "label": "cargo check (workspace)", + "label": "cargo check (workspace all targets)", "type": "cargo", "command": "check", - "args": ["--workspace"], + "args": ["--workspace", "--all-targets"], "problemMatcher": ["$rustc"], "group": "build" }, { - "label": "cargo clippy (workspace)", + "label": "cargo clippy (workspace strict)", "type": "cargo", "command": "clippy", - "args": ["--workspace"], + "args": ["--workspace", "--all-targets", "--", "-D", "warnings"], "problemMatcher": ["$rustc"], "group": "build" }, { - "label": "cargo clippy (foundation strict)", - "type": "cargo", - "command": "clippy", - "args": ["-p", "game", "-p", "shared", "--", "-D", "warnings"], - "problemMatcher": ["$rustc"], - "group": "build" - }, - { - "label": "cargo test (foundation)", + "label": "cargo test (workspace)", "type": "cargo", "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"], "group": "test" }, @@ -45,7 +64,7 @@ "label": "build game (debug)", "type": "cargo", "command": "build", - "args": ["-p", "game"], + "args": ["-p", "game", "--bin", "game"], "problemMatcher": ["$rustc"], "group": "build" }, @@ -53,47 +72,50 @@ "label": "build editor (debug)", "type": "cargo", "command": "build", - "args": ["-p", "editor"], + "args": ["-p", "editor", "--bin", "editor"], "problemMatcher": ["$rustc"], - "group": { "kind": "build", "isDefault": true } + "group": "build" }, { "label": "build editor (dev fast-link)", "type": "cargo", "command": "build", - "args": ["-p", "editor", "--features", "dev"], + "args": ["-p", "editor", "--bin", "editor", "--features", "dev"], "options": { - "env": { - "CARGO_TARGET_DIR": "${workspaceFolder}/target" - } + "cwd": "${workspaceFolder}", + "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" } }, "problemMatcher": ["$rustc"], - "group": "build" + "group": { "kind": "build", "isDefault": true } }, { "label": "build game (dev fast-link)", "type": "cargo", "command": "build", - "args": ["-p", "game", "--features", "dev"], + "args": ["-p", "game", "--bin", "game", "--features", "dev"], "options": { - "env": { - "CARGO_TARGET_DIR": "${workspaceFolder}/target" - } + "cwd": "${workspaceFolder}", + "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" } }, "problemMatcher": ["$rustc"], "group": "build" }, { - "label": "clean build editor (dev)", - "type": "shell", - "command": "cargo clean -p editor && cargo build -p editor --features dev", + "label": "clean dev-link crates", + "type": "cargo", + "command": "clean", + "args": ["-p", "editor", "-p", "game", "-p", "game_hot", "-p", "shared"], "options": { "cwd": "${workspaceFolder}", - "env": { - "CARGO_TARGET_DIR": "${workspaceFolder}/target" - } + "env": { "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" }, { @@ -102,9 +124,7 @@ "command": "cargo clean-target", "options": { "cwd": "${workspaceFolder}", - "env": { - "CARGO_TARGET_DIR": "${workspaceFolder}/target" - } + "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" } }, "problemMatcher": [], "group": "build" @@ -115,9 +135,7 @@ "command": "cargo clean-target --apply", "options": { "cwd": "${workspaceFolder}", - "env": { - "CARGO_TARGET_DIR": "${workspaceFolder}/target" - } + "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" } }, "problemMatcher": [], "group": "build" @@ -128,9 +146,7 @@ "command": "cargo clean-target --include-artifacts --days 3 --apply", "options": { "cwd": "${workspaceFolder}", - "env": { - "CARGO_TARGET_DIR": "${workspaceFolder}/target" - } + "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" } }, "problemMatcher": [], "group": "build" @@ -139,10 +155,11 @@ "label": "run editor (dev fast-link)", "type": "cargo", "command": "run", - "args": ["-p", "editor", "--features", "dev"], + "args": ["-p", "editor", "--bin", "editor", "--features", "dev"], "options": { + "cwd": "${workspaceFolder}", "env": { - "BEVY_FPS_HDR": "0", + "RUST_BACKTRACE": "1", "WGPU_VALIDATION": "0", "CARGO_TARGET_DIR": "${workspaceFolder}/target" } @@ -154,9 +171,83 @@ "label": "run game (dev fast-link)", "type": "cargo", "command": "run", - "args": ["-p", "game", "--features", "dev"], + "args": ["-p", "game", "--bin", "game", "--features", "dev"], "options": { + "cwd": "${workspaceFolder}", "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", "WGPU_VALIDATION": "0", "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\"", "options": { "cwd": "${workspaceFolder}", - "env": { - "CARGO_TARGET_DIR": "${workspaceFolder}/target" - } + "env": { "CARGO_TARGET_DIR": "${workspaceFolder}/target" } }, "problemMatcher": ["$rustc"], "group": "build", - "isBackground": true + "isBackground": true, + "detail": "Requires cargo-watch: cargo install cargo-watch --locked" }, { - "label": "verify foundation", + "label": "verify workspace", "dependsOrder": "sequence", "dependsOn": [ - "cargo fmt (check)", - "cargo check (workspace)", - "cargo clippy (workspace)", - "cargo clippy (foundation strict)", - "cargo test (foundation)", + "cargo fmt (workspace check)", + "cargo check (workspace all targets)", + "cargo clippy (workspace strict)", + "cargo test (workspace)", + "cargo clippy (launch feature matrix)", + "validate level scenes", "build game (debug)", "build editor (debug)" ], "problemMatcher": [], - "group": { "kind": "build", "isDefault": true } + "group": "build" } ] } diff --git a/README.md b/README.md index 81b9c85..63b6f8f 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,12 @@ native Bevy scene save/load, and BRP support for external tooling. ```bash # Check everything -cargo fmt --check -cargo check --workspace -cargo clippy --workspace -cargo clippy -p game -p shared -p protocol -p sim -- -D warnings -cargo test -p game --lib -cargo test -p sim +cargo fmt --all --check +cargo check --workspace --all-targets +cargo clippy --workspace --all-targets -- -D warnings +cargo clippy -p editor -p game --bins --features dev,hot-reload -- -D warnings +cargo test --workspace +cargo validate-levels # Runtime 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 ``` -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 | |-------|--------| @@ -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. - 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. -- 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. -- 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. +- 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 -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 @@ -238,8 +238,8 @@ The `.vscode/` folder is preconfigured: - `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, and excludes `target/` from search/watch. -- `tasks.json` provides workspace check/clippy plus game/editor build/run tasks. -- `launch.json` provides CodeLLDB launch configs for the editor and game. +- `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` 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. ## Architecture Decisions diff --git a/crates/editor/src/assets/thumbnails/cache.rs b/crates/editor/src/assets/thumbnails/cache.rs index 7b21fd6..e50eaac 100644 --- a/crates/editor/src/assets/thumbnails/cache.rs +++ b/crates/editor/src/assets/thumbnails/cache.rs @@ -6,7 +6,7 @@ use bevy::prelude::*; use bevy_egui::{egui, EguiPrimaryContextPass, EguiTextureHandle, EguiUserTextures}; 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::ThumbnailJobSource; use crate::assets::{ @@ -100,13 +100,12 @@ impl AssetThumbnailCache { return; } - if let Some(texture_path) = gltf_base_color_texture_path(&model_path) { - self.request_texture(key, texture_path, asset_server); + if !self.model_source_ready(&key, &model_path) { return; } - if !model_file_exists(&model_path) { - self.mark_studio_failed(&key, "file not found", false); + if let Some(texture_path) = gltf_base_color_texture_path(&model_path) { + self.request_texture(key, texture_path, asset_server); return; } @@ -130,8 +129,7 @@ impl AssetThumbnailCache { { return; } - if !model_file_exists(&model_path) { - self.mark_studio_failed(&key, "file not found", false); + if !self.model_source_ready(&key, &model_path) { return; } if studio.enqueue_source( @@ -160,8 +158,7 @@ impl AssetThumbnailCache { { return; } - if !model_file_exists(&model_path) { - self.mark_studio_failed(&key, "file not found", false); + if !self.model_source_ready(&key, &model_path) { return; } 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( &mut self, key: &str, diff --git a/crates/editor/src/assets/thumbnails/sources/gltf.rs b/crates/editor/src/assets/thumbnails/sources/gltf.rs index 91f100a..088c48a 100644 --- a/crates/editor/src/assets/thumbnails/sources/gltf.rs +++ b/crates/editor/src/assets/thumbnails/sources/gltf.rs @@ -29,6 +29,41 @@ pub fn gltf_base_color_texture_path(model_asset_path: &str) -> Option { 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; fn is_gltf_path(path: &Path) -> bool { @@ -61,7 +96,9 @@ fn normalize_asset_path(path: &Path) -> Option { #[cfg(test)] mod tests { use super::*; + use std::fs; use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn normalize_asset_path_keeps_assets_prefix() { @@ -79,4 +116,25 @@ mod tests { 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(); + } } diff --git a/crates/editor/src/assets/thumbnails/studio.rs b/crates/editor/src/assets/thumbnails/studio.rs index a2b82e7..108df7f 100644 --- a/crates/editor/src/assets/thumbnails/studio.rs +++ b/crates/editor/src/assets/thumbnails/studio.rs @@ -315,6 +315,9 @@ fn process_thumbnail_studio( ThumbnailStudioLayer, RenderLayers::layer(THUMBNAIL_LAYER), Transform::default(), + Visibility::Visible, + InheritedVisibility::default(), + ViewVisibility::default(), )) .id(); diff --git a/crates/game/src/lib.rs b/crates/game/src/lib.rs index 5ebae6b..c27e397 100644 --- a/crates/game/src/lib.rs +++ b/crates/game/src/lib.rs @@ -1,5 +1,12 @@ //! Runtime game crate: FPS player, rendering, and starter world. #![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 launch; @@ -62,8 +69,8 @@ mod systems { mod hot { use avian3d::prelude::*; use bevy::input::mouse::AccumulatedMouseMotion; + use bevy::light::atmosphere::ScatteringMedium; use bevy::light::CascadeShadowConfig; - use bevy::pbr::ScatteringMedium; use bevy::prelude::*; use bevy::time::Fixed; use bevy::window::{CursorOptions, PrimaryWindow}; diff --git a/docs/editor/README.md b/docs/editor/README.md index b8f5885..b6f4763 100644 --- a/docs/editor/README.md +++ b/docs/editor/README.md @@ -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). - **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. -- **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. - **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.