Expand asset browser subasset workflow
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
This commit is contained in:
parent
f29712d158
commit
3e053a81ab
1406
.cursor/plans/jackdaw_feature_roadmap_2026-06-06.plan.md
Normal file
1406
.cursor/plans/jackdaw_feature_roadmap_2026-06-06.plan.md
Normal file
File diff suppressed because it is too large
Load Diff
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
# Editor/runtime generated session state
|
# Editor/runtime generated session state
|
||||||
/assets/levels/.pie_session.scn.ron
|
/assets/levels/.pie_session.scn.ron
|
||||||
|
/assets/.trash/
|
||||||
|
|
||||||
# Backup, temporary, and log files
|
# Backup, temporary, and log files
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1478,8 +1478,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy_render"
|
name = "bevy_render"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "243523e33fe5dfcebc4240b1eb2fc16e855c5d4c0ea6a8393910740956770f44"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"bevy_app",
|
"bevy_app",
|
||||||
|
|||||||
@ -45,6 +45,8 @@ settings = { path = "crates/settings" }
|
|||||||
scene = { path = "crates/scene" }
|
scene = { path = "crates/scene" }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
# Linux surface acquire timeouts can be transient on Wayland/Xwayland drivers.
|
||||||
|
bevy_render = { path = "third_party/bevy_render" }
|
||||||
# Atmosphere-aware mesh view bind groups (WYSIWYG editor + transform gizmos).
|
# Atmosphere-aware mesh view bind groups (WYSIWYG editor + transform gizmos).
|
||||||
transform-gizmo-bevy = { path = "third_party/transform-gizmo-bevy" }
|
transform-gizmo-bevy = { path = "third_party/transform-gizmo-bevy" }
|
||||||
|
|
||||||
|
|||||||
26
README.md
26
README.md
@ -82,6 +82,7 @@ VS Code tasks: **target cleanup (dry run)** and **target cleanup (safe apply)**.
|
|||||||
|
|
||||||
- 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.
|
||||||
|
- The workspace patches `bevy_render` locally so transient Linux swapchain acquire timeouts skip one frame instead of panicking in `prepare_windows`.
|
||||||
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
@ -110,12 +111,12 @@ VS Code tasks: **target cleanup (dry run)** and **target cleanup (safe apply)**.
|
|||||||
| `W` / `E` / `R` | Translate / rotate / scale gizmo |
|
| `W` / `E` / `R` | Translate / rotate / scale gizmo |
|
||||||
| `X` | Toggle world/local gizmo orientation |
|
| `X` | Toggle world/local gizmo orientation |
|
||||||
| Viewport toolbar (sun / brush / box icons) | Shading: Lit, Unlit (albedo), Colliders (mesh off) |
|
| Viewport toolbar (sun / brush / box icons) | Shading: Lit, Unlit (albedo), Colliders (mesh off) |
|
||||||
| Viewport eye/options | Toggle actor root icon categories, colliders, lights, spawns, prefab/model anchors, and runtime player/camera visualizers |
|
| Viewport eye/options | Toggle actor root icon categories, adjust icon/gizmo size, and control colliders, lights, spawns, prefab/model anchors, and runtime player/camera visualizers |
|
||||||
| `Tab` in viewport | Cycle selection through overlapping objects at last click |
|
| `Tab` in viewport | Cycle selection through overlapping objects at last click |
|
||||||
| Click Player visualizer in Edit mode | Select or create the authored `PlayerSpawn` (`Player Start`) |
|
| Click Player visualizer in Edit mode | Select or create the authored `PlayerSpawn` (`Player Start`) |
|
||||||
| Select Project Sun | Inspect project default lighting; create a scene sun override |
|
| Select Project Sun | Inspect project default lighting; create a scene sun override |
|
||||||
| Asset Browser project/file views | Browse `assets/`, search/filter/sort, switch grid/list, inspect import settings; drag assets into the viewport to place |
|
| Asset Browser project/file views | Browse `assets/`, search/filter/sort, switch grid/list, expand model subassets with generated thumbnails, inspect staged import/material settings, drag assets/submeshes into the viewport |
|
||||||
| Asset Browser texture action | Assign base-color texture to selection |
|
| Asset Browser context/details actions | Apply textures/materials, regenerate thumbnails, reimport models, place assets/submeshes, or move file assets to `assets/.trash/` |
|
||||||
| `Ctrl+P` | Command palette (`scene.reset_lighting`, `selection.group`, `selection.focus`, play commands) |
|
| `Ctrl+P` | Command palette (`scene.reset_lighting`, `selection.group`, `selection.focus`, play commands) |
|
||||||
| `F7` | While paused in Play: advance one sim tick |
|
| `F7` | While paused in Play: advance one sim tick |
|
||||||
| Shift/Ctrl + click (Hierarchy) | Additive selection |
|
| Shift/Ctrl + click (Hierarchy) | Additive selection |
|
||||||
@ -141,6 +142,7 @@ VS Code tasks: **target cleanup (dry run)** and **target cleanup (safe apply)**.
|
|||||||
respected.
|
respected.
|
||||||
- Add a `PlayerSpawn` marker component (via Inspector) on a level object to choose where Play
|
- Add a `PlayerSpawn` marker component (via Inspector) on a level object to choose where Play
|
||||||
starts; otherwise the default game spawn `(0, 1.5, 10)` is used.
|
starts; otherwise the default game spawn `(0, 1.5, 10)` is used.
|
||||||
|
- Actor root icons draw over scene meshes, stay screen-sized while zooming, and are prioritized when clicked.
|
||||||
- Clean game-view overlay (`G`) hides editor widgets, actor root icons, visualizers, selectable proxies, gizmos,
|
- Clean game-view overlay (`G`) hides editor widgets, actor root icons, visualizers, selectable proxies, gizmos,
|
||||||
selection outlines, and grid without changing the active camera.
|
selection outlines, and grid without changing the active camera.
|
||||||
- **F5** stops Play and restores **player sim state** only; authored scene edits made during PIE
|
- **F5** stops Play and restores **player sim state** only; authored scene edits made during PIE
|
||||||
@ -174,7 +176,9 @@ VS Code tasks: **target cleanup (dry run)** and **target cleanup (safe apply)**.
|
|||||||
artifacts in `assets/meshes/generated/`. Renderer slots reference imported content-browser mesh
|
artifacts in `assets/meshes/generated/`. Renderer slots reference imported content-browser mesh
|
||||||
and material assets, while generated collision is stored separately in `ColliderDesc` plus
|
and material assets, while generated collision is stored separately in `ColliderDesc` plus
|
||||||
`RigidBodyDesc`. Switch a model asset's placement mode to **Scene Instance** in Asset Browser
|
`RigidBodyDesc`. Switch a model asset's placement mode to **Scene Instance** in Asset Browser
|
||||||
details when you need full `SceneRoot` loading for animation/skinning/scene data.
|
details when you need full `SceneRoot` loading for animation/skinning/scene data. Expanding a
|
||||||
|
model in the Asset Browser exposes normalized mesh/material/texture subassets; dragging a mesh
|
||||||
|
subasset places that part through the same static mesh renderer path.
|
||||||
- Runtime-only handles/colliders are not serialized directly, keeping scenes stable and portable.
|
- Runtime-only handles/colliders are not serialized directly, keeping scenes stable and portable.
|
||||||
- Editor-only cameras and helper roots are filtered from selection, hierarchy, and scene save.
|
- Editor-only cameras and helper roots are filtered from selection, hierarchy, and scene save.
|
||||||
- **PIE restores player sim only** (transform, velocity, jump state) when you stop Play; authored
|
- **PIE restores player sim only** (transform, velocity, jump state) when you stop Play; authored
|
||||||
@ -252,7 +256,7 @@ crates/
|
|||||||
- [x] Mesh picking selection + highlights
|
- [x] Mesh picking selection + highlights
|
||||||
- [x] Transform gizmos
|
- [x] Transform gizmos
|
||||||
- [x] Editor-side scene visualizers for colliders, lights, player spawns, prefab/model anchors, and runtime player/cameras
|
- [x] Editor-side scene visualizers for colliders, lights, player spawns, prefab/model anchors, and runtime player/cameras
|
||||||
- [x] Selectable actor root icons in the 3D viewport with per-category visibility controls
|
- [x] Selectable, screen-sized actor root icons in the 3D viewport with per-category visibility controls
|
||||||
- [x] Filtered hierarchy, inspector, viewport, toolbar, asset browser, and status panels
|
- [x] Filtered hierarchy, inspector, viewport, toolbar, asset browser, and status panels
|
||||||
- [x] Delete, duplicate, rename, and structural/material undo-redo
|
- [x] Delete, duplicate, rename, and structural/material undo-redo
|
||||||
- [x] Native Bevy scene New/Open/Save/Save As with dirty title tracking
|
- [x] Native Bevy scene New/Open/Save/Save As with dirty title tracking
|
||||||
@ -268,6 +272,7 @@ crates/
|
|||||||
- [x] Determinism harness: same inputs over same ticks produce the same state summary/hash
|
- [x] Determinism harness: same inputs over same ticks produce the same state summary/hash
|
||||||
- [x] Verify: `cargo fmt --check` / `cargo check --workspace` / `cargo clippy --workspace` / strict foundation clippy / `cargo test -p sim`
|
- [x] Verify: `cargo fmt --check` / `cargo check --workspace` / `cargo clippy --workspace` / strict foundation clippy / `cargo test -p sim`
|
||||||
- [x] Stable asset registry with UUIDs + import settings in asset browser details
|
- [x] Stable asset registry with UUIDs + import settings in asset browser details
|
||||||
|
- [x] Asset Browser expandable model subasset shelves, independent mesh/material/texture thumbnails, staged import/material details with shader-schema parameters, context actions, and trash-first file removal
|
||||||
- [x] Prefab instances (`PrefabInstance`) + save-as-prefab + unpack
|
- [x] Prefab instances (`PrefabInstance`) + save-as-prefab + unpack
|
||||||
- [x] Unsaved-scene confirm on New/Open/Recent
|
- [x] Unsaved-scene confirm on New/Open/Recent
|
||||||
- [x] Hierarchy multi-select, reparent undo, multi-entity gizmo transform
|
- [x] Hierarchy multi-select, reparent undo, multi-entity gizmo transform
|
||||||
@ -294,14 +299,17 @@ crates/
|
|||||||
- Crouch lowers the camera and movement speed; the collider stays full-height for stability.
|
- Crouch lowers the camera and movement speed; the collider stays full-height for stability.
|
||||||
- The editor asset browser is filesystem-backed with folder/tree navigation, grid/list views,
|
- The editor asset browser is filesystem-backed with folder/tree navigation, grid/list views,
|
||||||
texture and model thumbnails (glTF albedo fast-path; offscreen render studio for FBX and
|
texture and model thumbnails (glTF albedo fast-path; offscreen render studio for FBX and
|
||||||
untextured models), search/filter/sort controls, and a details pane. **File → Import Assets**
|
untextured models), material sphere thumbnails, search/filter/sort controls, expandable model
|
||||||
|
subasset shelves, and a staged details pane. **File → Import Assets**
|
||||||
accepts glTF/GLB and **FBX** (binary; copies sibling `.fbm` texture folders when present).
|
accepts glTF/GLB and **FBX** (binary; copies sibling `.fbm` texture folders when present).
|
||||||
Model assets generate normalized static mesh manifests under `assets/meshes/generated/`; drag/drop
|
Model assets generate normalized static mesh manifests under `assets/meshes/generated/`; drag/drop
|
||||||
uses `StaticMeshRenderer` by default with imported asset refs and optional separate static mesh
|
uses `StaticMeshRenderer` by default with imported asset refs and optional separate static mesh
|
||||||
collider components. Asset details can switch placement to **Scene Instance** for `ModelRef`/`SceneRoot`
|
collider components. Expanded mesh subassets generate independent thumbnails and can be placed independently. Asset details can switch
|
||||||
playback. Skeletal animation playback is not supported by the static mesh path yet.
|
placement to **Scene Instance** for `ModelRef`/`SceneRoot` playback, shared material assets can be
|
||||||
|
edited from the browser, and delete actions move files to `assets/.trash/`. Skeletal animation
|
||||||
|
playback is not supported by the static mesh path yet.
|
||||||
- Prefab instances use stable asset IDs (`PrefabInstance` + registry) with serialized `PrefabOverrides` (transform, material, per-child visibility by name). Inspector **Apply/Revert** per field group; **Unpack** removes the instance link.
|
- Prefab instances use stable asset IDs (`PrefabInstance` + registry) with serialized `PrefabOverrides` (transform, material, per-child visibility by name). Inspector **Apply/Revert** per field group; **Unpack** removes the instance link.
|
||||||
- Material assets are RON files under `assets/materials/`; shader schemas live under `assets/shaders/`. `MaterialDesc` stores shader kind, typed parameters, texture bindings, and StandardMaterial fields. Actor inspector edits create per-actor `MaterialOverride` data for static mesh slots instead of mutating shared material assets.
|
- Material assets are RON files under `assets/materials/`; shader schemas live under `assets/shaders/`. `MaterialDesc` stores shader kind, typed parameters, texture bindings, and StandardMaterial fields. The Asset Browser material details editor can apply shader schemas, edit typed parameters/textures, and regenerate sphere thumbnails. Actor inspector edits create per-actor `MaterialOverride` data for static mesh slots instead of mutating shared material assets.
|
||||||
- Per-field reflect undo for all components remains future work; typed `shared` inspectors cover the common authoring path.
|
- Per-field reflect undo for all components remains future work; typed `shared` inspectors cover the common authoring path.
|
||||||
- The authoring/hydration layer is intentionally small so richer asset workflows (terrain,
|
- The authoring/hydration layer is intentionally small so richer asset workflows (terrain,
|
||||||
material graphs, lighting profiles) can be added without changing the scene format foundation.
|
material graphs, lighting profiles) can be added without changing the scene format foundation.
|
||||||
|
|||||||
@ -32,34 +32,87 @@
|
|||||||
dependencies: [],
|
dependencies: [],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
id: ("b8b43f26-0eee-4ee8-9e16-53aedbc69205"),
|
id: ("b98ef565-3500-49e7-9935-f685fa9b2594"),
|
||||||
path: "assets/models/Room3.fbx",
|
path: "assets/models/painted_wooden_chair_02_2k.fbx",
|
||||||
label: "Room3",
|
label: "painted_wooden_chair_02_2k",
|
||||||
kind_tag: "Model",
|
|
||||||
import_settings: (
|
|
||||||
scale: 1.7,
|
|
||||||
generate_collider: true,
|
|
||||||
lod0_only: true,
|
|
||||||
placement_mode: StaticAsset,
|
|
||||||
hierarchy_mode: SingleActor,
|
|
||||||
material_policy: AuthoringOverride,
|
|
||||||
static_mesh_manifest_path: Some("assets/meshes/generated/b8b43f26-0eee-4ee8-9e16-53aedbc69205.static_mesh.ron"),
|
|
||||||
),
|
|
||||||
dependencies: [],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: ("582cd326-c32d-49da-9424-ae38b3d60648"),
|
|
||||||
path: "assets/models/Dumpster.fbx",
|
|
||||||
label: "Dumpster",
|
|
||||||
kind_tag: "Model",
|
kind_tag: "Model",
|
||||||
import_settings: (
|
import_settings: (
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
generate_collider: true,
|
generate_collider: true,
|
||||||
lod0_only: true,
|
lod0_only: true,
|
||||||
placement_mode: StaticAsset,
|
placement_mode: StaticAsset,
|
||||||
hierarchy_mode: SourceHierarchy,
|
hierarchy_mode: SingleActor,
|
||||||
material_policy: AuthoringOverride,
|
material_policy: SourceMaterials,
|
||||||
static_mesh_manifest_path: Some("assets/meshes/generated/582cd326-c32d-49da-9424-ae38b3d60648.static_mesh.ron"),
|
static_mesh_manifest_path: Some("assets/meshes/generated/b98ef565-3500-49e7-9935-f685fa9b2594.static_mesh.ron"),
|
||||||
|
),
|
||||||
|
dependencies: [],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("71071fc3-7a7f-454d-b9ab-93ebf0feccbe"),
|
||||||
|
path: "assets/models/metal_stool_01_2k.gltf",
|
||||||
|
label: "metal_stool_01_2k",
|
||||||
|
kind_tag: "Model",
|
||||||
|
import_settings: (
|
||||||
|
scale: 1.0,
|
||||||
|
generate_collider: true,
|
||||||
|
lod0_only: true,
|
||||||
|
placement_mode: SceneInstance,
|
||||||
|
hierarchy_mode: SingleActor,
|
||||||
|
material_policy: SourceMaterials,
|
||||||
|
static_mesh_manifest_path: Some("assets/meshes/generated/71071fc3-7a7f-454d-b9ab-93ebf0feccbe.static_mesh.ron"),
|
||||||
|
),
|
||||||
|
dependencies: [
|
||||||
|
"assets/models/metal_stool_01.bin",
|
||||||
|
"assets/models/textures/metal_stool_01_arm_2k.jpg",
|
||||||
|
"assets/models/textures/metal_stool_01_diff_2k.jpg",
|
||||||
|
"assets/models/textures/metal_stool_01_nor_gl_2k.jpg",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("c4abe41c-ac88-4aec-8168-27e8a7c90583"),
|
||||||
|
path: "assets/textures/metal_stool_01_nor_gl_2k.jpg",
|
||||||
|
label: "metal_stool_01_nor_gl_2k",
|
||||||
|
kind_tag: "Texture",
|
||||||
|
import_settings: (
|
||||||
|
scale: 1.0,
|
||||||
|
generate_collider: true,
|
||||||
|
lod0_only: true,
|
||||||
|
placement_mode: StaticAsset,
|
||||||
|
hierarchy_mode: SingleActor,
|
||||||
|
material_policy: SourceMaterials,
|
||||||
|
static_mesh_manifest_path: None,
|
||||||
|
),
|
||||||
|
dependencies: [],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("0496e62e-1837-4940-a9c9-b2b4ffe9ed90"),
|
||||||
|
path: "assets/textures/metal_stool_01_arm_2k.jpg",
|
||||||
|
label: "metal_stool_01_arm_2k",
|
||||||
|
kind_tag: "Texture",
|
||||||
|
import_settings: (
|
||||||
|
scale: 1.0,
|
||||||
|
generate_collider: true,
|
||||||
|
lod0_only: true,
|
||||||
|
placement_mode: StaticAsset,
|
||||||
|
hierarchy_mode: SingleActor,
|
||||||
|
material_policy: SourceMaterials,
|
||||||
|
static_mesh_manifest_path: None,
|
||||||
|
),
|
||||||
|
dependencies: [],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
id: ("bddaa4d4-5097-42bf-8d51-dd9a3580442a"),
|
||||||
|
path: "assets/textures/metal_stool_01_diff_2k.jpg",
|
||||||
|
label: "metal_stool_01_diff_2k",
|
||||||
|
kind_tag: "Texture",
|
||||||
|
import_settings: (
|
||||||
|
scale: 1.0,
|
||||||
|
generate_collider: true,
|
||||||
|
lod0_only: true,
|
||||||
|
placement_mode: StaticAsset,
|
||||||
|
hierarchy_mode: SingleActor,
|
||||||
|
material_policy: SourceMaterials,
|
||||||
|
static_mesh_manifest_path: None,
|
||||||
),
|
),
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,82 +0,0 @@
|
|||||||
(
|
|
||||||
schema_version: 1,
|
|
||||||
asset_id: "582cd326-c32d-49da-9424-ae38b3d60648",
|
|
||||||
label: "Dumpster",
|
|
||||||
source: (
|
|
||||||
path: "assets/models/Dumpster.fbx",
|
|
||||||
format: "fbx",
|
|
||||||
fingerprint: (
|
|
||||||
byte_len: 50940,
|
|
||||||
modified_unix_secs: 1780338936,
|
|
||||||
),
|
|
||||||
dependencies: [],
|
|
||||||
),
|
|
||||||
import: (
|
|
||||||
scale: 1.0,
|
|
||||||
generate_collider: true,
|
|
||||||
lod0_only: true,
|
|
||||||
placement_mode: StaticAsset,
|
|
||||||
hierarchy_mode: SourceHierarchy,
|
|
||||||
material_policy: AuthoringOverride,
|
|
||||||
),
|
|
||||||
metadata: (
|
|
||||||
mesh_count: 3,
|
|
||||||
material_count: 2,
|
|
||||||
node_count: 4,
|
|
||||||
animation_count: 0,
|
|
||||||
skin_count: 0,
|
|
||||||
light_count: 0,
|
|
||||||
camera_count: 0,
|
|
||||||
),
|
|
||||||
parts: [
|
|
||||||
(
|
|
||||||
id: "mesh:mesh1000",
|
|
||||||
name: "Dumpster / Material 0",
|
|
||||||
mesh_label: "Mesh1000",
|
|
||||||
material_id: Some("material:material0"),
|
|
||||||
material_slot_name: "Dumpster",
|
|
||||||
material_label: Some("Material0"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (8.342552, 0.06418517, -6.7325354),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node1"),
|
|
||||||
source_mesh: Some("Mesh1"),
|
|
||||||
source_material: Some("Dumpster"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh2000",
|
|
||||||
name: "Dumpster Lid / Material 0",
|
|
||||||
mesh_label: "Mesh2000",
|
|
||||||
material_id: Some("material:material1"),
|
|
||||||
material_slot_name: "Dumpster Lid",
|
|
||||||
material_label: Some("Material1"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (7.362827, 2.5433145, -7.784438),
|
|
||||||
rotation: (0.70710677, 0.000000000082318055, -0.00000000008231806, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node2"),
|
|
||||||
source_mesh: Some("Mesh2"),
|
|
||||||
source_material: Some("Dumpster Lid"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh3000",
|
|
||||||
name: "Dumpster Lid_02 / Material 0",
|
|
||||||
mesh_label: "Mesh3000",
|
|
||||||
material_id: Some("material:material1"),
|
|
||||||
material_slot_name: "Dumpster Lid",
|
|
||||||
material_label: Some("Material1"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (9.351767, 2.5433145, -7.784438),
|
|
||||||
rotation: (0.70710677, 0.000000000082318055, -0.00000000008231806, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node3"),
|
|
||||||
source_mesh: Some("Mesh3"),
|
|
||||||
source_material: Some("Dumpster Lid"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
warnings: [],
|
|
||||||
)
|
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
(
|
||||||
|
schema_version: 1,
|
||||||
|
asset_id: "71071fc3-7a7f-454d-b9ab-93ebf0feccbe",
|
||||||
|
label: "metal_stool_01_2k",
|
||||||
|
source: (
|
||||||
|
path: "assets/models/metal_stool_01_2k.gltf",
|
||||||
|
format: "gltf",
|
||||||
|
fingerprint: (
|
||||||
|
byte_len: 2790,
|
||||||
|
modified_unix_secs: 1780712587,
|
||||||
|
),
|
||||||
|
dependencies: [
|
||||||
|
"assets/models/metal_stool_01.bin",
|
||||||
|
"assets/models/textures/metal_stool_01_arm_2k.jpg",
|
||||||
|
"assets/models/textures/metal_stool_01_diff_2k.jpg",
|
||||||
|
"assets/models/textures/metal_stool_01_nor_gl_2k.jpg",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
import: (
|
||||||
|
scale: 1.0,
|
||||||
|
generate_collider: true,
|
||||||
|
lod0_only: true,
|
||||||
|
placement_mode: SceneInstance,
|
||||||
|
hierarchy_mode: SingleActor,
|
||||||
|
material_policy: SourceMaterials,
|
||||||
|
),
|
||||||
|
metadata: (
|
||||||
|
mesh_count: 1,
|
||||||
|
material_count: 1,
|
||||||
|
node_count: 1,
|
||||||
|
animation_count: 0,
|
||||||
|
skin_count: 0,
|
||||||
|
light_count: 0,
|
||||||
|
camera_count: 0,
|
||||||
|
),
|
||||||
|
parts: [
|
||||||
|
(
|
||||||
|
id: "mesh:mesh0_primitive0",
|
||||||
|
name: "metal_stool_01 / Primitive 0",
|
||||||
|
mesh_label: "Mesh0/Primitive0",
|
||||||
|
material_id: Some("material:material0"),
|
||||||
|
material_slot_name: "metal_stool_01",
|
||||||
|
material_label: Some("Material0"),
|
||||||
|
local_transform: (
|
||||||
|
translation: (0.0, 0.0, 0.0),
|
||||||
|
rotation: (0.0, 0.0, 0.0, 1.0),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
source_node: Some("Node0"),
|
||||||
|
source_mesh: Some("Mesh0"),
|
||||||
|
source_material: Some("metal_stool_01"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
warnings: [],
|
||||||
|
)
|
||||||
@ -1,770 +0,0 @@
|
|||||||
(
|
|
||||||
schema_version: 1,
|
|
||||||
asset_id: "b8b43f26-0eee-4ee8-9e16-53aedbc69205",
|
|
||||||
label: "Room3",
|
|
||||||
source: (
|
|
||||||
path: "assets/models/Room3.fbx",
|
|
||||||
format: "fbx",
|
|
||||||
fingerprint: (
|
|
||||||
byte_len: 159836,
|
|
||||||
modified_unix_secs: 1780196194,
|
|
||||||
),
|
|
||||||
dependencies: [],
|
|
||||||
),
|
|
||||||
import: (
|
|
||||||
scale: 1.7,
|
|
||||||
generate_collider: true,
|
|
||||||
lod0_only: true,
|
|
||||||
placement_mode: StaticAsset,
|
|
||||||
hierarchy_mode: SingleActor,
|
|
||||||
material_policy: AuthoringOverride,
|
|
||||||
),
|
|
||||||
metadata: (
|
|
||||||
mesh_count: 23,
|
|
||||||
material_count: 11,
|
|
||||||
node_count: 24,
|
|
||||||
animation_count: 0,
|
|
||||||
skin_count: 0,
|
|
||||||
light_count: 0,
|
|
||||||
camera_count: 0,
|
|
||||||
),
|
|
||||||
parts: [
|
|
||||||
(
|
|
||||||
id: "mesh:mesh1000",
|
|
||||||
name: "Windows Small.000 / Material 0",
|
|
||||||
mesh_label: "Mesh1000",
|
|
||||||
material_id: Some("material:material0"),
|
|
||||||
material_slot_name: "Window Frame Metal.002",
|
|
||||||
material_label: Some("Material0"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (2.9808052, 1.9115452, -0.40523553),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248338, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node1"),
|
|
||||||
source_mesh: Some("Mesh1"),
|
|
||||||
source_material: Some("Window Frame Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh10000",
|
|
||||||
name: "Window Right Up.000 / Material 0",
|
|
||||||
mesh_label: "Mesh10000",
|
|
||||||
material_id: Some("material:material0"),
|
|
||||||
material_slot_name: "Window Frame Metal.002",
|
|
||||||
material_label: Some("Material0"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-0.97121567, 1.8132615, 3.0487669),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248341, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node10"),
|
|
||||||
source_mesh: Some("Mesh10"),
|
|
||||||
source_material: Some("Window Frame Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh10001",
|
|
||||||
name: "Window Right Up.000 / Material 1",
|
|
||||||
mesh_label: "Mesh10001",
|
|
||||||
material_id: Some("material:material1"),
|
|
||||||
material_slot_name: "Window Glass.002",
|
|
||||||
material_label: Some("Material1"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-0.97121567, 1.8132615, 3.0487669),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248341, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node10"),
|
|
||||||
source_mesh: Some("Mesh10"),
|
|
||||||
source_material: Some("Window Glass.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh1001",
|
|
||||||
name: "Windows Small.000 / Material 1",
|
|
||||||
mesh_label: "Mesh1001",
|
|
||||||
material_id: Some("material:material1"),
|
|
||||||
material_slot_name: "Window Glass.002",
|
|
||||||
material_label: Some("Material1"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (2.9808052, 1.9115452, -0.40523553),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248338, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node1"),
|
|
||||||
source_mesh: Some("Mesh1"),
|
|
||||||
source_material: Some("Window Glass.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh11000",
|
|
||||||
name: "Window Right Low.000 / Material 0",
|
|
||||||
mesh_label: "Mesh11000",
|
|
||||||
material_id: Some("material:material0"),
|
|
||||||
material_slot_name: "Window Frame Metal.002",
|
|
||||||
material_label: Some("Material0"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-0.9734232, 1.4140832, 3.0185344),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248341, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node11"),
|
|
||||||
source_mesh: Some("Mesh11"),
|
|
||||||
source_material: Some("Window Frame Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh11001",
|
|
||||||
name: "Window Right Low.000 / Material 1",
|
|
||||||
mesh_label: "Mesh11001",
|
|
||||||
material_id: Some("material:material1"),
|
|
||||||
material_slot_name: "Window Glass.002",
|
|
||||||
material_label: Some("Material1"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-0.9734232, 1.4140832, 3.0185344),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248341, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node11"),
|
|
||||||
source_mesh: Some("Mesh11"),
|
|
||||||
source_material: Some("Window Glass.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh12000",
|
|
||||||
name: "Window Left Up.000 / Material 0",
|
|
||||||
mesh_label: "Mesh12000",
|
|
||||||
material_id: Some("material:material0"),
|
|
||||||
material_slot_name: "Window Frame Metal.002",
|
|
||||||
material_label: Some("Material0"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (1.583923, 1.8132616, 3.0487669),
|
|
||||||
rotation: (0.70710677, 0.000000034125748, -0.000000025061802, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248341, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node12"),
|
|
||||||
source_mesh: Some("Mesh12"),
|
|
||||||
source_material: Some("Window Frame Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh12001",
|
|
||||||
name: "Window Left Up.000 / Material 1",
|
|
||||||
mesh_label: "Mesh12001",
|
|
||||||
material_id: Some("material:material1"),
|
|
||||||
material_slot_name: "Window Glass.002",
|
|
||||||
material_label: Some("Material1"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (1.583923, 1.8132616, 3.0487669),
|
|
||||||
rotation: (0.70710677, 0.000000034125748, -0.000000025061802, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248341, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node12"),
|
|
||||||
source_mesh: Some("Mesh12"),
|
|
||||||
source_material: Some("Window Glass.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh13000",
|
|
||||||
name: "Window Left Low.000 / Material 0",
|
|
||||||
mesh_label: "Mesh13000",
|
|
||||||
material_id: Some("material:material0"),
|
|
||||||
material_slot_name: "Window Frame Metal.002",
|
|
||||||
material_label: Some("Material0"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (1.5816854, 1.414083, 3.0185344),
|
|
||||||
rotation: (0.70710677, 0.000000034125748, -0.000000025061802, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248341, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node13"),
|
|
||||||
source_mesh: Some("Mesh13"),
|
|
||||||
source_material: Some("Window Frame Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh13001",
|
|
||||||
name: "Window Left Low.000 / Material 1",
|
|
||||||
mesh_label: "Mesh13001",
|
|
||||||
material_id: Some("material:material1"),
|
|
||||||
material_slot_name: "Window Glass.002",
|
|
||||||
material_label: Some("Material1"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (1.5816854, 1.414083, 3.0185344),
|
|
||||||
rotation: (0.70710677, 0.000000034125748, -0.000000025061802, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248341, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node13"),
|
|
||||||
source_mesh: Some("Mesh13"),
|
|
||||||
source_material: Some("Window Glass.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh14000",
|
|
||||||
name: "Window Center Low.000 / Material 0",
|
|
||||||
mesh_label: "Mesh14000",
|
|
||||||
material_id: Some("material:material0"),
|
|
||||||
material_slot_name: "Window Frame Metal.002",
|
|
||||||
material_label: Some("Material0"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.29577515, 1.4927816, 3.0203688),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248343, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node14"),
|
|
||||||
source_mesh: Some("Mesh14"),
|
|
||||||
source_material: Some("Window Frame Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh14001",
|
|
||||||
name: "Window Center Low.000 / Material 1",
|
|
||||||
mesh_label: "Mesh14001",
|
|
||||||
material_id: Some("material:material1"),
|
|
||||||
material_slot_name: "Window Glass.002",
|
|
||||||
material_label: Some("Material1"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.29577515, 1.4927816, 3.0203688),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248343, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node14"),
|
|
||||||
source_mesh: Some("Mesh14"),
|
|
||||||
source_material: Some("Window Glass.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh15000",
|
|
||||||
name: "Window Cente Up.000 / Material 0",
|
|
||||||
mesh_label: "Mesh15000",
|
|
||||||
material_id: Some("material:material0"),
|
|
||||||
material_slot_name: "Window Frame Metal.002",
|
|
||||||
material_label: Some("Material0"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.2986794, 1.7560294, 3.0472102),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248343, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node15"),
|
|
||||||
source_mesh: Some("Mesh15"),
|
|
||||||
source_material: Some("Window Frame Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh15001",
|
|
||||||
name: "Window Cente Up.000 / Material 1",
|
|
||||||
mesh_label: "Mesh15001",
|
|
||||||
material_id: Some("material:material1"),
|
|
||||||
material_slot_name: "Window Glass.002",
|
|
||||||
material_label: Some("Material1"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.2986794, 1.7560294, 3.0472102),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248343, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node15"),
|
|
||||||
source_mesh: Some("Mesh15"),
|
|
||||||
source_material: Some("Window Glass.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh16000",
|
|
||||||
name: "Lights.Potlight.004 / Material 0",
|
|
||||||
mesh_label: "Mesh16000",
|
|
||||||
material_id: Some("material:material7"),
|
|
||||||
material_slot_name: "Light Metal.002",
|
|
||||||
material_label: Some("Material7"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (1.9999996, 2.9877658, 1.1368884),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node16"),
|
|
||||||
source_mesh: Some("Mesh16"),
|
|
||||||
source_material: Some("Light Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh16002",
|
|
||||||
name: "Lights.Potlight.004 / Material 2",
|
|
||||||
mesh_label: "Mesh16002",
|
|
||||||
material_id: Some("material:material9"),
|
|
||||||
material_slot_name: "Light.002",
|
|
||||||
material_label: Some("Material9"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (1.9999996, 2.9877658, 1.1368884),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node16"),
|
|
||||||
source_mesh: Some("Mesh16"),
|
|
||||||
source_material: Some("Light.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh17000",
|
|
||||||
name: "Lights.Potlight.003 / Material 0",
|
|
||||||
mesh_label: "Mesh17000",
|
|
||||||
material_id: Some("material:material7"),
|
|
||||||
material_slot_name: "Light Metal.002",
|
|
||||||
material_label: Some("Material7"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (1.9999996, 2.9877653, -1.8631115),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node17"),
|
|
||||||
source_mesh: Some("Mesh17"),
|
|
||||||
source_material: Some("Light Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh17002",
|
|
||||||
name: "Lights.Potlight.003 / Material 2",
|
|
||||||
mesh_label: "Mesh17002",
|
|
||||||
material_id: Some("material:material9"),
|
|
||||||
material_slot_name: "Light.002",
|
|
||||||
material_label: Some("Material9"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (1.9999996, 2.9877653, -1.8631115),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node17"),
|
|
||||||
source_mesh: Some("Mesh17"),
|
|
||||||
source_material: Some("Light.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh18000",
|
|
||||||
name: "Lights.Potlight.002 / Material 0",
|
|
||||||
mesh_label: "Mesh18000",
|
|
||||||
material_id: Some("material:material7"),
|
|
||||||
material_slot_name: "Light Metal.002",
|
|
||||||
material_label: Some("Material7"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-1.8000002, 2.9877653, -1.8631115),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node18"),
|
|
||||||
source_mesh: Some("Mesh18"),
|
|
||||||
source_material: Some("Light Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh18002",
|
|
||||||
name: "Lights.Potlight.002 / Material 2",
|
|
||||||
mesh_label: "Mesh18002",
|
|
||||||
material_id: Some("material:material9"),
|
|
||||||
material_slot_name: "Light.002",
|
|
||||||
material_label: Some("Material9"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-1.8000002, 2.9877653, -1.8631115),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node18"),
|
|
||||||
source_mesh: Some("Mesh18"),
|
|
||||||
source_material: Some("Light.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh19000",
|
|
||||||
name: "Lights.Potlight.001 / Material 0",
|
|
||||||
mesh_label: "Mesh19000",
|
|
||||||
material_id: Some("material:material7"),
|
|
||||||
material_slot_name: "Light Metal.002",
|
|
||||||
material_label: Some("Material7"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-1.8000002, 2.9877658, 1.1368884),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node19"),
|
|
||||||
source_mesh: Some("Mesh19"),
|
|
||||||
source_material: Some("Light Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh19002",
|
|
||||||
name: "Lights.Potlight.001 / Material 2",
|
|
||||||
mesh_label: "Mesh19002",
|
|
||||||
material_id: Some("material:material9"),
|
|
||||||
material_slot_name: "Light.002",
|
|
||||||
material_label: Some("Material9"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-1.8000002, 2.9877658, 1.1368884),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node19"),
|
|
||||||
source_mesh: Some("Mesh19"),
|
|
||||||
source_material: Some("Light.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh2000",
|
|
||||||
name: "Window Right.000 / Material 0",
|
|
||||||
mesh_label: "Mesh2000",
|
|
||||||
material_id: Some("material:defaultmaterial"),
|
|
||||||
material_slot_name: "Default Material",
|
|
||||||
material_label: Some("DefaultMaterial"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-0.9659626, 1.613703, 3.0355246),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248338, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node2"),
|
|
||||||
source_mesh: Some("Mesh2"),
|
|
||||||
source_material: Some("Default Material"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh20000",
|
|
||||||
name: "Lights.Center.003 / Material 0",
|
|
||||||
mesh_label: "Mesh20000",
|
|
||||||
material_id: Some("material:material7"),
|
|
||||||
material_slot_name: "Light Metal.002",
|
|
||||||
material_label: Some("Material7"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.24156909, 2.792474, -0.63835967),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node20"),
|
|
||||||
source_mesh: Some("Mesh20"),
|
|
||||||
source_material: Some("Light Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh20001",
|
|
||||||
name: "Lights.Center.003 / Material 1",
|
|
||||||
mesh_label: "Mesh20001",
|
|
||||||
material_id: Some("material:material8"),
|
|
||||||
material_slot_name: "Matte Black.002",
|
|
||||||
material_label: Some("Material8"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.24156909, 2.792474, -0.63835967),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node20"),
|
|
||||||
source_mesh: Some("Mesh20"),
|
|
||||||
source_material: Some("Matte Black.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh20002",
|
|
||||||
name: "Lights.Center.003 / Material 2",
|
|
||||||
mesh_label: "Mesh20002",
|
|
||||||
material_id: Some("material:material9"),
|
|
||||||
material_slot_name: "Light.002",
|
|
||||||
material_label: Some("Material9"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.24156909, 2.792474, -0.63835967),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node20"),
|
|
||||||
source_mesh: Some("Mesh20"),
|
|
||||||
source_material: Some("Light.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh21000",
|
|
||||||
name: "Lights.Center.002 / Material 0",
|
|
||||||
mesh_label: "Mesh21000",
|
|
||||||
material_id: Some("material:material7"),
|
|
||||||
material_slot_name: "Light Metal.002",
|
|
||||||
material_label: Some("Material7"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.25059184, 2.7924743, 0.36630905),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node21"),
|
|
||||||
source_mesh: Some("Mesh21"),
|
|
||||||
source_material: Some("Light Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh21001",
|
|
||||||
name: "Lights.Center.002 / Material 1",
|
|
||||||
mesh_label: "Mesh21001",
|
|
||||||
material_id: Some("material:material8"),
|
|
||||||
material_slot_name: "Matte Black.002",
|
|
||||||
material_label: Some("Material8"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.25059184, 2.7924743, 0.36630905),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node21"),
|
|
||||||
source_mesh: Some("Mesh21"),
|
|
||||||
source_material: Some("Matte Black.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh21002",
|
|
||||||
name: "Lights.Center.002 / Material 2",
|
|
||||||
mesh_label: "Mesh21002",
|
|
||||||
material_id: Some("material:material9"),
|
|
||||||
material_slot_name: "Light.002",
|
|
||||||
material_label: Some("Material9"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.25059184, 2.7924743, 0.36630905),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node21"),
|
|
||||||
source_mesh: Some("Mesh21"),
|
|
||||||
source_material: Some("Light.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh22000",
|
|
||||||
name: "Lights.Center.001 / Material 0",
|
|
||||||
mesh_label: "Mesh22000",
|
|
||||||
material_id: Some("material:material7"),
|
|
||||||
material_slot_name: "Light Metal.002",
|
|
||||||
material_label: Some("Material7"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.21980214, 2.7924743, -0.15153252),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node22"),
|
|
||||||
source_mesh: Some("Mesh22"),
|
|
||||||
source_material: Some("Light Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh22001",
|
|
||||||
name: "Lights.Center.001 / Material 1",
|
|
||||||
mesh_label: "Mesh22001",
|
|
||||||
material_id: Some("material:material8"),
|
|
||||||
material_slot_name: "Matte Black.002",
|
|
||||||
material_label: Some("Material8"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.21980214, 2.7924743, -0.15153252),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node22"),
|
|
||||||
source_mesh: Some("Mesh22"),
|
|
||||||
source_material: Some("Matte Black.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh22002",
|
|
||||||
name: "Lights.Center.001 / Material 2",
|
|
||||||
mesh_label: "Mesh22002",
|
|
||||||
material_id: Some("material:material9"),
|
|
||||||
material_slot_name: "Light.002",
|
|
||||||
material_label: Some("Material9"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.21980214, 2.7924743, -0.15153252),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node22"),
|
|
||||||
source_mesh: Some("Mesh22"),
|
|
||||||
source_material: Some("Light.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh23002",
|
|
||||||
name: "Door_Knob.000 / Material 2",
|
|
||||||
mesh_label: "Mesh23002",
|
|
||||||
material_id: Some("material:material10"),
|
|
||||||
material_slot_name: "Door Knob Gold.002",
|
|
||||||
material_label: Some("Material10"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-3.0024662, 1.0415587, -1.6272027),
|
|
||||||
rotation: (0.70710677, -0.0000000023899738, 0.00000000007193627, -0.7071067),
|
|
||||||
scale: (0.07870353, 0.45364702, 0.45364702),
|
|
||||||
),
|
|
||||||
source_node: Some("Node23"),
|
|
||||||
source_mesh: Some("Mesh23"),
|
|
||||||
source_material: Some("Door Knob Gold.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh3000",
|
|
||||||
name: "Window Left.000 / Material 0",
|
|
||||||
mesh_label: "Mesh3000",
|
|
||||||
material_id: Some("material:defaultmaterial"),
|
|
||||||
material_slot_name: "Default Material",
|
|
||||||
material_label: Some("DefaultMaterial"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (1.5851622, 1.613703, 3.0355246),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248338, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node3"),
|
|
||||||
source_mesh: Some("Mesh3"),
|
|
||||||
source_material: Some("Default Material"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh4000",
|
|
||||||
name: "Window Frames.000 / Material 0",
|
|
||||||
mesh_label: "Mesh4000",
|
|
||||||
material_id: Some("material:material2"),
|
|
||||||
material_slot_name: "Painted Wooden Frame.002",
|
|
||||||
material_label: Some("Material2"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.0, 0.0027079582, 0.0),
|
|
||||||
rotation: (0.5000001, -0.49999994, -0.5, -0.50000006),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node4"),
|
|
||||||
source_mesh: Some("Mesh4"),
|
|
||||||
source_material: Some("Painted Wooden Frame.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh5000",
|
|
||||||
name: "Window Center.000 / Material 0",
|
|
||||||
mesh_label: "Mesh5000",
|
|
||||||
material_id: Some("material:defaultmaterial"),
|
|
||||||
material_slot_name: "Default Material",
|
|
||||||
material_label: Some("DefaultMaterial"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.3107944, 1.613703, 3.0355246),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2387476, 0.9248338, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node5"),
|
|
||||||
source_mesh: Some("Mesh5"),
|
|
||||||
source_material: Some("Default Material"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh6000",
|
|
||||||
name: "Room.000 / Material 0",
|
|
||||||
mesh_label: "Mesh6000",
|
|
||||||
material_id: Some("material:material2"),
|
|
||||||
material_slot_name: "Painted Wooden Frame.002",
|
|
||||||
material_label: Some("Material2"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.0, 0.0027079582, 0.0),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node6"),
|
|
||||||
source_mesh: Some("Mesh6"),
|
|
||||||
source_material: Some("Painted Wooden Frame.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh6001",
|
|
||||||
name: "Room.000 / Material 1",
|
|
||||||
mesh_label: "Mesh6001",
|
|
||||||
material_id: Some("material:material3"),
|
|
||||||
material_slot_name: "Ceiling.002",
|
|
||||||
material_label: Some("Material3"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.0, 0.0027079582, 0.0),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node6"),
|
|
||||||
source_mesh: Some("Mesh6"),
|
|
||||||
source_material: Some("Ceiling.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh6002",
|
|
||||||
name: "Room.000 / Material 2",
|
|
||||||
mesh_label: "Mesh6002",
|
|
||||||
material_id: Some("material:material4"),
|
|
||||||
material_slot_name: "Hardwood Floor.002",
|
|
||||||
material_label: Some("Material4"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.0, 0.0027079582, 0.0),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node6"),
|
|
||||||
source_mesh: Some("Mesh6"),
|
|
||||||
source_material: Some("Hardwood Floor.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh6003",
|
|
||||||
name: "Room.000 / Material 3",
|
|
||||||
mesh_label: "Mesh6003",
|
|
||||||
material_id: Some("material:material5"),
|
|
||||||
material_slot_name: "Painted Wall.002",
|
|
||||||
material_label: Some("Material5"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.0, 0.0027079582, 0.0),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node6"),
|
|
||||||
source_mesh: Some("Mesh6"),
|
|
||||||
source_material: Some("Painted Wall.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh6004",
|
|
||||||
name: "Room.000 / Material 4",
|
|
||||||
mesh_label: "Mesh6004",
|
|
||||||
material_id: Some("material:material6"),
|
|
||||||
material_slot_name: "Brick Accent Wall.002",
|
|
||||||
material_label: Some("Material6"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.0, 0.0027079582, 0.0),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node6"),
|
|
||||||
source_mesh: Some("Mesh6"),
|
|
||||||
source_material: Some("Brick Accent Wall.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh7000",
|
|
||||||
name: "Lights.000 / Material 0",
|
|
||||||
mesh_label: "Mesh7000",
|
|
||||||
material_id: Some("material:material7"),
|
|
||||||
material_slot_name: "Light Metal.002",
|
|
||||||
material_label: Some("Material7"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (0.24510838, 2.9453187, -0.14346208),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.0, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node7"),
|
|
||||||
source_mesh: Some("Mesh7"),
|
|
||||||
source_material: Some("Light Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh8000",
|
|
||||||
name: "Door Frame.000 / Material 0",
|
|
||||||
mesh_label: "Mesh8000",
|
|
||||||
material_id: Some("material:material2"),
|
|
||||||
material_slot_name: "Painted Wooden Frame.002",
|
|
||||||
material_label: Some("Material2"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-3.0060542, 0.0027079582, -2.1066966),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (1.2365791, 1.0, 1.0),
|
|
||||||
),
|
|
||||||
source_node: Some("Node8"),
|
|
||||||
source_mesh: Some("Mesh8"),
|
|
||||||
source_material: Some("Painted Wooden Frame.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh9000",
|
|
||||||
name: "Door.000 / Material 0",
|
|
||||||
mesh_label: "Mesh9000",
|
|
||||||
material_id: Some("material:material2"),
|
|
||||||
material_slot_name: "Painted Wooden Frame.002",
|
|
||||||
material_label: Some("Material2"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-2.9729986, 0.22508162, -2.6792114),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (0.07870354, 0.45364702, 0.45364702),
|
|
||||||
),
|
|
||||||
source_node: Some("Node9"),
|
|
||||||
source_mesh: Some("Mesh9"),
|
|
||||||
source_material: Some("Painted Wooden Frame.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh9001",
|
|
||||||
name: "Door.000 / Material 1",
|
|
||||||
mesh_label: "Mesh9001",
|
|
||||||
material_id: Some("material:material7"),
|
|
||||||
material_slot_name: "Light Metal.002",
|
|
||||||
material_label: Some("Material7"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-2.9729986, 0.22508162, -2.6792114),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (0.07870354, 0.45364702, 0.45364702),
|
|
||||||
),
|
|
||||||
source_node: Some("Node9"),
|
|
||||||
source_mesh: Some("Mesh9"),
|
|
||||||
source_material: Some("Light Metal.002"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
id: "mesh:mesh9002",
|
|
||||||
name: "Door.000 / Material 2",
|
|
||||||
mesh_label: "Mesh9002",
|
|
||||||
material_id: Some("material:material10"),
|
|
||||||
material_slot_name: "Door Knob Gold.002",
|
|
||||||
material_label: Some("Material10"),
|
|
||||||
local_transform: (
|
|
||||||
translation: (-2.9729986, 0.22508162, -2.6792114),
|
|
||||||
rotation: (0.70710677, 0.0, 0.0, -0.7071067),
|
|
||||||
scale: (0.07870354, 0.45364702, 0.45364702),
|
|
||||||
),
|
|
||||||
source_node: Some("Node9"),
|
|
||||||
source_mesh: Some("Mesh9"),
|
|
||||||
source_material: Some("Door Knob Gold.002"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
warnings: [],
|
|
||||||
)
|
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
(
|
||||||
|
schema_version: 1,
|
||||||
|
asset_id: "b98ef565-3500-49e7-9935-f685fa9b2594",
|
||||||
|
label: "painted_wooden_chair_02_2k",
|
||||||
|
source: (
|
||||||
|
path: "assets/models/painted_wooden_chair_02_2k.fbx",
|
||||||
|
format: "fbx",
|
||||||
|
fingerprint: (
|
||||||
|
byte_len: 59964,
|
||||||
|
modified_unix_secs: 1780713434,
|
||||||
|
),
|
||||||
|
dependencies: [],
|
||||||
|
),
|
||||||
|
import: (
|
||||||
|
scale: 1.0,
|
||||||
|
generate_collider: true,
|
||||||
|
lod0_only: true,
|
||||||
|
placement_mode: StaticAsset,
|
||||||
|
hierarchy_mode: SingleActor,
|
||||||
|
material_policy: SourceMaterials,
|
||||||
|
),
|
||||||
|
metadata: (
|
||||||
|
mesh_count: 1,
|
||||||
|
material_count: 1,
|
||||||
|
node_count: 2,
|
||||||
|
animation_count: 0,
|
||||||
|
skin_count: 0,
|
||||||
|
light_count: 0,
|
||||||
|
camera_count: 0,
|
||||||
|
),
|
||||||
|
parts: [
|
||||||
|
(
|
||||||
|
id: "mesh:mesh1000",
|
||||||
|
name: "painted_wooden_chair_02 / Material 0",
|
||||||
|
mesh_label: "Mesh1000",
|
||||||
|
material_id: Some("material:material0"),
|
||||||
|
material_slot_name: "painted_wooden_chair_02",
|
||||||
|
material_label: Some("Material0"),
|
||||||
|
local_transform: (
|
||||||
|
translation: (0.0, 0.0, 0.0),
|
||||||
|
rotation: (0.5, 0.50000006, 0.5, -0.50000006),
|
||||||
|
scale: (1.0, 1.0, 1.0),
|
||||||
|
),
|
||||||
|
source_node: Some("Node1"),
|
||||||
|
source_mesh: Some("Mesh1"),
|
||||||
|
source_material: Some("painted_wooden_chair_02"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
warnings: [],
|
||||||
|
)
|
||||||
BIN
assets/models/Dumpster.fbx
(Stored with Git LFS)
BIN
assets/models/Dumpster.fbx
(Stored with Git LFS)
Binary file not shown.
BIN
assets/models/Room3.fbx
(Stored with Git LFS)
BIN
assets/models/Room3.fbx
(Stored with Git LFS)
Binary file not shown.
BIN
assets/models/metal_stool_01_2k.gltf
(Stored with Git LFS)
Normal file
BIN
assets/models/metal_stool_01_2k.gltf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/models/painted_wooden_chair_02_2k.fbx
(Stored with Git LFS)
Normal file
BIN
assets/models/painted_wooden_chair_02_2k.fbx
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/textures/metal_stool_01_arm_2k.jpg
(Stored with Git LFS)
Normal file
BIN
assets/textures/metal_stool_01_arm_2k.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/textures/metal_stool_01_diff_2k.jpg
(Stored with Git LFS)
Normal file
BIN
assets/textures/metal_stool_01_diff_2k.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/textures/metal_stool_01_nor_gl_2k.jpg
(Stored with Git LFS)
Normal file
BIN
assets/textures/metal_stool_01_nor_gl_2k.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
24
crates/editor/assets/shaders/actor_icon_overlay.wgsl
Normal file
24
crates/editor/assets/shaders/actor_icon_overlay.wgsl
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#import bevy_pbr::forward_io::VertexOutput
|
||||||
|
|
||||||
|
struct ActorIconOverlayMaterial {
|
||||||
|
brightness: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(0)
|
||||||
|
var icon_texture: texture_2d<f32>;
|
||||||
|
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(1)
|
||||||
|
var icon_sampler: sampler;
|
||||||
|
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(2)
|
||||||
|
var<uniform> material: ActorIconOverlayMaterial;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let texel = textureSample(icon_texture, icon_sampler, in.uv);
|
||||||
|
if texel.a < 0.01 {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec4(texel.rgb * material.brightness, texel.a);
|
||||||
|
}
|
||||||
@ -11,7 +11,8 @@ use shared::{
|
|||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::asset_db::{
|
use crate::asset_db::{
|
||||||
find_asset_by_path, find_asset_mut_by_path, AssetRegistry, ModelPlacementMode,
|
find_asset_by_path, find_asset_mut_by_path, AssetRegistry, MaterialImportPolicy,
|
||||||
|
ModelPlacementMode,
|
||||||
};
|
};
|
||||||
use crate::assets::static_mesh::{
|
use crate::assets::static_mesh::{
|
||||||
load_static_mesh_manifest, refresh_static_mesh_artifact, renderer_from_manifest,
|
load_static_mesh_manifest, refresh_static_mesh_artifact, renderer_from_manifest,
|
||||||
@ -21,10 +22,24 @@ use crate::history::{spawn_with_history, EditorEntitySnapshot};
|
|||||||
pub const BUILTINS_FOLDER: &str = "__builtins__";
|
pub const BUILTINS_FOLDER: &str = "__builtins__";
|
||||||
pub const ASSETS_ROOT: &str = "assets";
|
pub const ASSETS_ROOT: &str = "assets";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum AssetSubAssetKind {
|
||||||
|
Mesh,
|
||||||
|
Material,
|
||||||
|
Texture,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum AssetSelection {
|
pub enum AssetSelection {
|
||||||
Builtin(String),
|
Builtin(String),
|
||||||
File(String),
|
File(String),
|
||||||
|
SubAsset {
|
||||||
|
parent_path: String,
|
||||||
|
sub_asset_id: String,
|
||||||
|
label: String,
|
||||||
|
kind: AssetSubAssetKind,
|
||||||
|
source_path: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssetSelection {
|
impl AssetSelection {
|
||||||
@ -34,6 +49,21 @@ impl AssetSelection {
|
|||||||
None => AssetSelection::Builtin(asset.label.clone()),
|
None => AssetSelection::Builtin(asset.label.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parent_path(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
AssetSelection::Builtin(_) => None,
|
||||||
|
AssetSelection::File(path) => Some(path),
|
||||||
|
AssetSelection::SubAsset { parent_path, .. } => Some(parent_path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_label(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
AssetSelection::Builtin(label) | AssetSelection::File(label) => label,
|
||||||
|
AssetSelection::SubAsset { label, .. } => label,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@ -137,6 +167,10 @@ impl EditorAssets {
|
|||||||
.assets
|
.assets
|
||||||
.iter()
|
.iter()
|
||||||
.find(|asset| asset.path.as_deref() == Some(path.as_str())),
|
.find(|asset| asset.path.as_deref() == Some(path.as_str())),
|
||||||
|
AssetSelection::SubAsset { parent_path, .. } => self
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|asset| asset.path.as_deref() == Some(parent_path.as_str())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,10 +186,20 @@ impl EditorAssets {
|
|||||||
.and_then(|selection| self.asset_for_selection(selection))
|
.and_then(|selection| self.asset_for_selection(selection))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dragging_selection(&self) -> Option<&AssetSelection> {
|
||||||
|
self.dragging.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select(&mut self, selection: AssetSelection) {
|
pub fn select(&mut self, selection: AssetSelection) {
|
||||||
if let Some(asset) = self.asset_for_selection(&selection).cloned() {
|
if let Some(asset) = self.asset_for_selection(&selection).cloned() {
|
||||||
|
let selected_label = match &selection {
|
||||||
|
AssetSelection::SubAsset { label, kind, .. } => {
|
||||||
|
format!("{kind:?}: {label}")
|
||||||
|
}
|
||||||
|
_ => asset.label.clone(),
|
||||||
|
};
|
||||||
self.selected = Some(selection);
|
self.selected = Some(selection);
|
||||||
self.status = format!("Selected {}", asset.label);
|
self.status = format!("Selected {selected_label}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +346,7 @@ fn should_skip_asset_path(path: &Path) -> bool {
|
|||||||
let normalized = normalize_asset_path(path);
|
let normalized = normalize_asset_path(path);
|
||||||
normalized == "assets/project.ron"
|
normalized == "assets/project.ron"
|
||||||
|| normalized.starts_with("assets/.index/")
|
|| normalized.starts_with("assets/.index/")
|
||||||
|
|| normalized.starts_with("assets/.trash/")
|
||||||
|| normalized.starts_with(crate::assets::static_mesh::STATIC_MESH_ARTIFACT_DIR)
|
|| normalized.starts_with(crate::assets::static_mesh::STATIC_MESH_ARTIFACT_DIR)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,6 +588,101 @@ pub fn spawn_asset_at(world: &mut World, asset: &EditorAsset, translation: Vec3)
|
|||||||
Some(spawn_with_history(world, snapshot))
|
Some(spawn_with_history(world, snapshot))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn spawn_subasset_at(
|
||||||
|
world: &mut World,
|
||||||
|
selection: &AssetSelection,
|
||||||
|
translation: Vec3,
|
||||||
|
) -> Option<Entity> {
|
||||||
|
let AssetSelection::SubAsset {
|
||||||
|
parent_path,
|
||||||
|
sub_asset_id,
|
||||||
|
label,
|
||||||
|
kind: AssetSubAssetKind::Mesh,
|
||||||
|
..
|
||||||
|
} = selection
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let record = world
|
||||||
|
.get_resource::<AssetRegistry>()
|
||||||
|
.and_then(|registry| find_asset_by_path(registry, parent_path))?;
|
||||||
|
let manifest_path = record
|
||||||
|
.import_settings
|
||||||
|
.static_mesh_manifest_path
|
||||||
|
.as_deref()?;
|
||||||
|
let manifest = load_static_mesh_manifest(manifest_path).ok()?;
|
||||||
|
let part = manifest
|
||||||
|
.parts
|
||||||
|
.iter()
|
||||||
|
.find(|part| part_effective_id_for_selection(part) == *sub_asset_id)?;
|
||||||
|
|
||||||
|
let mesh_ref = shared::EditorAssetRef::new(
|
||||||
|
manifest.asset_id.clone(),
|
||||||
|
sub_asset_id.clone(),
|
||||||
|
label.clone(),
|
||||||
|
);
|
||||||
|
let material = matches!(
|
||||||
|
record.import_settings.material_policy,
|
||||||
|
MaterialImportPolicy::SourceMaterials
|
||||||
|
)
|
||||||
|
.then(|| {
|
||||||
|
part_effective_material_id_for_selection(part).map(|id| {
|
||||||
|
shared::EditorAssetRef::new(
|
||||||
|
manifest.asset_id.clone(),
|
||||||
|
id,
|
||||||
|
part.material_slot_name.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
let slot = shared::StaticMeshRendererEntry {
|
||||||
|
id: shared::ComponentInstanceId::new("slot:0"),
|
||||||
|
name: label.clone(),
|
||||||
|
mesh: mesh_ref.clone(),
|
||||||
|
material,
|
||||||
|
local_transform: part.local_transform,
|
||||||
|
visible: true,
|
||||||
|
cast_shadows: true,
|
||||||
|
receive_shadows: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut snapshot = EditorEntitySnapshot {
|
||||||
|
actor_id: None,
|
||||||
|
actor_kind: ActorKind::StaticMesh,
|
||||||
|
actor_name: None,
|
||||||
|
name: Some(label.clone()),
|
||||||
|
transform: Transform::from_translation(translation + Vec3::Y * 0.1),
|
||||||
|
primitive: None,
|
||||||
|
static_mesh_renderer: Some(shared::StaticMeshRenderer::single(slot)),
|
||||||
|
material: None,
|
||||||
|
material_override: None,
|
||||||
|
rigid_body: None,
|
||||||
|
collider: None,
|
||||||
|
physics: None,
|
||||||
|
light: None,
|
||||||
|
player_spawn: false,
|
||||||
|
model: None,
|
||||||
|
prefab: None,
|
||||||
|
prefab_instance: None,
|
||||||
|
weapon_spawn: None,
|
||||||
|
trigger_volume: None,
|
||||||
|
post_process_volume: None,
|
||||||
|
team_spawn: None,
|
||||||
|
objective: None,
|
||||||
|
hierarchy_sibling_index: 0,
|
||||||
|
editor_visibility: EditorVisibility::default(),
|
||||||
|
children: Vec::new(),
|
||||||
|
};
|
||||||
|
snapshot.transform.scale *= record.import_settings.scale;
|
||||||
|
if record.import_settings.generate_collider {
|
||||||
|
snapshot.rigid_body = Some(RigidBodyDesc::default());
|
||||||
|
snapshot.collider = Some(ColliderDesc::static_mesh(vec![mesh_ref]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(spawn_with_history(world, snapshot))
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_static_mesh_placement_mode(
|
fn apply_static_mesh_placement_mode(
|
||||||
snapshot: &mut EditorEntitySnapshot,
|
snapshot: &mut EditorEntitySnapshot,
|
||||||
renderer: shared::StaticMeshRenderer,
|
renderer: shared::StaticMeshRenderer,
|
||||||
@ -619,6 +759,24 @@ fn static_mesh_collider_for_renderer(renderer: &shared::StaticMeshRenderer) -> C
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn part_effective_id_for_selection(part: &crate::assets::static_mesh::StaticMeshPart) -> String {
|
||||||
|
if part.id.trim().is_empty() {
|
||||||
|
crate::assets::static_mesh::part_id_from_label(&part.mesh_label)
|
||||||
|
} else {
|
||||||
|
part.id.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn part_effective_material_id_for_selection(
|
||||||
|
part: &crate::assets::static_mesh::StaticMeshPart,
|
||||||
|
) -> Option<String> {
|
||||||
|
part.material_id.clone().or_else(|| {
|
||||||
|
part.material_label
|
||||||
|
.as_ref()
|
||||||
|
.map(|label| crate::assets::static_mesh::material_id_from_label(label))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn static_mesh_renderer_for_asset(
|
fn static_mesh_renderer_for_asset(
|
||||||
world: &mut World,
|
world: &mut World,
|
||||||
path: &str,
|
path: &str,
|
||||||
|
|||||||
@ -11,5 +11,5 @@ pub mod thumbnails;
|
|||||||
pub use catalog::*;
|
pub use catalog::*;
|
||||||
pub use thumbnails::{
|
pub use thumbnails::{
|
||||||
draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon, prefetch_folder_thumbnails,
|
draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon, prefetch_folder_thumbnails,
|
||||||
AssetThumbnailCache, ThumbnailCacheSnapshot, ThumbnailState, ThumbnailsPlugin,
|
AssetThumbnailCache, ThumbnailCacheSnapshot, ThumbnailState, ThumbnailStudio, ThumbnailsPlugin,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,6 +8,7 @@ use egui_phosphor_icons::icons;
|
|||||||
|
|
||||||
use super::sources::gltf::gltf_base_color_texture_path;
|
use super::sources::gltf::gltf_base_color_texture_path;
|
||||||
use super::studio::{model_file_exists, ThumbnailStudio};
|
use super::studio::{model_file_exists, ThumbnailStudio};
|
||||||
|
use super::ThumbnailJobSource;
|
||||||
use crate::assets::{
|
use crate::assets::{
|
||||||
asset_cache_key, asset_server_path, EditorAsset, EditorAssetKind, EditorAssets,
|
asset_cache_key, asset_server_path, EditorAsset, EditorAssetKind, EditorAssets,
|
||||||
};
|
};
|
||||||
@ -114,6 +115,97 @@ impl AssetThumbnailCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn request_mesh_subasset(
|
||||||
|
&mut self,
|
||||||
|
key: String,
|
||||||
|
model_path: String,
|
||||||
|
mesh_label: String,
|
||||||
|
material_label: Option<String>,
|
||||||
|
studio: &mut ThumbnailStudio,
|
||||||
|
) {
|
||||||
|
if matches!(
|
||||||
|
self.state(&key),
|
||||||
|
Some(ThumbnailState::Ready | ThumbnailState::Pending)
|
||||||
|
) || self.failed.contains_key(&key)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !model_file_exists(&model_path) {
|
||||||
|
self.mark_studio_failed(&key, "file not found", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if studio.enqueue_source(
|
||||||
|
key.clone(),
|
||||||
|
ThumbnailJobSource::MeshSubAsset {
|
||||||
|
model_path,
|
||||||
|
mesh_label,
|
||||||
|
material_label,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
self.studio_pending.insert(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_source_material(
|
||||||
|
&mut self,
|
||||||
|
key: String,
|
||||||
|
model_path: String,
|
||||||
|
material_label: String,
|
||||||
|
studio: &mut ThumbnailStudio,
|
||||||
|
) {
|
||||||
|
if matches!(
|
||||||
|
self.state(&key),
|
||||||
|
Some(ThumbnailState::Ready | ThumbnailState::Pending)
|
||||||
|
) || self.failed.contains_key(&key)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !model_file_exists(&model_path) {
|
||||||
|
self.mark_studio_failed(&key, "file not found", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if studio.enqueue_source(
|
||||||
|
key.clone(),
|
||||||
|
ThumbnailJobSource::SourceMaterial {
|
||||||
|
model_path,
|
||||||
|
material_label,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
self.studio_pending.insert(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_material_asset(
|
||||||
|
&mut self,
|
||||||
|
key: String,
|
||||||
|
path: String,
|
||||||
|
studio: &mut ThumbnailStudio,
|
||||||
|
) {
|
||||||
|
if matches!(
|
||||||
|
self.state(&key),
|
||||||
|
Some(ThumbnailState::Ready | ThumbnailState::Pending)
|
||||||
|
) || self.failed.contains_key(&key)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let material = match shared::MaterialAsset::load_from_path(&path) {
|
||||||
|
Ok(asset) => asset,
|
||||||
|
Err(error) => {
|
||||||
|
self.mark_studio_failed(&key, &error, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if studio.enqueue_source(
|
||||||
|
key.clone(),
|
||||||
|
ThumbnailJobSource::MaterialAsset {
|
||||||
|
label: material.label.clone(),
|
||||||
|
material: material.material,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
self.studio_pending.insert(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn complete_studio_thumbnail(
|
pub(crate) fn complete_studio_thumbnail(
|
||||||
&mut self,
|
&mut self,
|
||||||
key: &str,
|
key: &str,
|
||||||
@ -297,6 +389,18 @@ pub struct ThumbnailCacheSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ThumbnailCacheSnapshot {
|
impl ThumbnailCacheSnapshot {
|
||||||
|
pub fn texture_for_key(&self, key: &str) -> Option<egui::TextureId> {
|
||||||
|
self.texture_ids.get(key).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_pending_key(&self, key: &str) -> bool {
|
||||||
|
self.pending_keys.iter().any(|pending| pending == key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_failed_key(&self, key: &str) -> bool {
|
||||||
|
self.failed_keys.iter().any(|failed| failed == key)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn texture_for(&self, asset: &EditorAsset) -> Option<egui::TextureId> {
|
pub fn texture_for(&self, asset: &EditorAsset) -> Option<egui::TextureId> {
|
||||||
self.texture_ids.get(&asset_cache_key(asset)).copied()
|
self.texture_ids.get(&asset_cache_key(asset)).copied()
|
||||||
}
|
}
|
||||||
@ -331,7 +435,7 @@ pub fn prefetch_folder_thumbnails(world: &mut World, folder: &str) {
|
|||||||
.filter(|asset| {
|
.filter(|asset| {
|
||||||
matches!(
|
matches!(
|
||||||
asset.kind,
|
asset.kind,
|
||||||
EditorAssetKind::Texture | EditorAssetKind::Model
|
EditorAssetKind::Texture | EditorAssetKind::Model | EditorAssetKind::Material
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.filter_map(|asset| {
|
.filter_map(|asset| {
|
||||||
@ -352,6 +456,9 @@ pub fn prefetch_folder_thumbnails(world: &mut World, folder: &str) {
|
|||||||
EditorAssetKind::Model => {
|
EditorAssetKind::Model => {
|
||||||
cache.request_model(key, path, &asset_server, &mut studio)
|
cache.request_model(key, path, &asset_server, &mut studio)
|
||||||
}
|
}
|
||||||
|
EditorAssetKind::Material => {
|
||||||
|
cache.request_material_asset(key, path, &mut studio)
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,40 @@
|
|||||||
//! Thumbnail generation job descriptor.
|
//! Thumbnail generation job descriptor.
|
||||||
|
|
||||||
|
use shared::MaterialDesc;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ThumbnailJob {
|
pub struct ThumbnailJob {
|
||||||
pub cache_key: String,
|
pub cache_key: String,
|
||||||
pub model_path: String,
|
pub source: ThumbnailJobSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ThumbnailJobSource {
|
||||||
|
Model {
|
||||||
|
model_path: String,
|
||||||
|
},
|
||||||
|
MeshSubAsset {
|
||||||
|
model_path: String,
|
||||||
|
mesh_label: String,
|
||||||
|
material_label: Option<String>,
|
||||||
|
},
|
||||||
|
SourceMaterial {
|
||||||
|
model_path: String,
|
||||||
|
material_label: String,
|
||||||
|
},
|
||||||
|
MaterialAsset {
|
||||||
|
label: String,
|
||||||
|
material: MaterialDesc,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThumbnailJobSource {
|
||||||
|
pub fn label(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Model { model_path }
|
||||||
|
| Self::MeshSubAsset { model_path, .. }
|
||||||
|
| Self::SourceMaterial { model_path, .. } => model_path,
|
||||||
|
Self::MaterialAsset { label, .. } => label,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ pub use cache::{
|
|||||||
draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon, prefetch_folder_thumbnails,
|
draw_asset_cell_with, invalidate_on_catalog_refresh, kind_icon, prefetch_folder_thumbnails,
|
||||||
AssetThumbnailCache, AssetThumbnailsPlugin, ThumbnailCacheSnapshot, ThumbnailState,
|
AssetThumbnailCache, AssetThumbnailsPlugin, ThumbnailCacheSnapshot, ThumbnailState,
|
||||||
};
|
};
|
||||||
pub use job::ThumbnailJob;
|
pub use job::{ThumbnailJob, ThumbnailJobSource};
|
||||||
pub use sources::{FbxThumbnailSource, GltfThumbnailSource, ThumbnailModelSource};
|
pub use sources::{FbxThumbnailSource, GltfThumbnailSource, ThumbnailModelSource};
|
||||||
pub use studio::{
|
pub use studio::{
|
||||||
model_file_exists, model_scene_asset_path, ThumbnailStudio, ThumbnailStudioPlugin,
|
model_file_exists, model_scene_asset_path, ThumbnailStudio, ThumbnailStudioPlugin,
|
||||||
|
|||||||
@ -12,10 +12,10 @@ use bevy::prelude::*;
|
|||||||
use bevy::render::render_resource::TextureFormat;
|
use bevy::render::render_resource::TextureFormat;
|
||||||
use bevy::scene::SceneRoot;
|
use bevy::scene::SceneRoot;
|
||||||
use bevy_egui::EguiUserTextures;
|
use bevy_egui::EguiUserTextures;
|
||||||
use shared::ModelRef;
|
use shared::{material_from_desc, ModelRef};
|
||||||
|
|
||||||
use super::cache::AssetThumbnailCache;
|
use super::cache::AssetThumbnailCache;
|
||||||
use super::job::ThumbnailJob;
|
use super::job::{ThumbnailJob, ThumbnailJobSource};
|
||||||
use super::sources::{source_for_extension, uses_scene_root};
|
use super::sources::{source_for_extension, uses_scene_root};
|
||||||
use crate::assets::asset_server_path;
|
use crate::assets::asset_server_path;
|
||||||
use crate::infra::EditorOnly;
|
use crate::infra::EditorOnly;
|
||||||
@ -52,7 +52,8 @@ pub struct ThumbnailStudio {
|
|||||||
|
|
||||||
struct ActiveModelThumbnail {
|
struct ActiveModelThumbnail {
|
||||||
cache_key: String,
|
cache_key: String,
|
||||||
model_path: String,
|
label: String,
|
||||||
|
render_image: Handle<Image>,
|
||||||
root: Entity,
|
root: Entity,
|
||||||
meshes_ready: bool,
|
meshes_ready: bool,
|
||||||
mesh_warmup_frames: u8,
|
mesh_warmup_frames: u8,
|
||||||
@ -75,6 +76,10 @@ impl Plugin for ThumbnailStudioPlugin {
|
|||||||
|
|
||||||
impl ThumbnailStudio {
|
impl ThumbnailStudio {
|
||||||
pub fn enqueue(&mut self, cache_key: String, model_path: String) -> bool {
|
pub fn enqueue(&mut self, cache_key: String, model_path: String) -> bool {
|
||||||
|
self.enqueue_source(cache_key, ThumbnailJobSource::Model { model_path })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enqueue_source(&mut self, cache_key: String, source: ThumbnailJobSource) -> bool {
|
||||||
if self
|
if self
|
||||||
.active
|
.active
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -85,10 +90,7 @@ impl ThumbnailStudio {
|
|||||||
if self.queue.iter().any(|job| job.cache_key == cache_key) {
|
if self.queue.iter().any(|job| job.cache_key == cache_key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.queue.push_back(ThumbnailJob {
|
self.queue.push_back(ThumbnailJob { cache_key, source });
|
||||||
cache_key,
|
|
||||||
model_path,
|
|
||||||
});
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,12 +124,7 @@ pub fn model_scene_asset_path(path: &str, scene_index: usize) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn setup_thumbnail_studio(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
|
fn setup_thumbnail_studio(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
|
||||||
let render_image = images.add(Image::new_target_texture(
|
let render_image = images.add(studio_render_image());
|
||||||
THUMB_SIZE,
|
|
||||||
THUMB_SIZE,
|
|
||||||
TextureFormat::Rgba8UnormSrgb,
|
|
||||||
None,
|
|
||||||
));
|
|
||||||
|
|
||||||
let camera = commands
|
let camera = commands
|
||||||
.spawn((
|
.spawn((
|
||||||
@ -200,8 +197,10 @@ fn process_thumbnail_studio(
|
|||||||
mut textures: ResMut<EguiUserTextures>,
|
mut textures: ResMut<EguiUserTextures>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
mut mesh_storage: ResMut<Assets<Mesh>>,
|
mut mesh_storage: ResMut<Assets<Mesh>>,
|
||||||
|
mut images: ResMut<Assets<Image>>,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
mut cameras: Query<&mut Camera, With<ThumbnailStudioCamera>>,
|
mut cameras: Query<&mut Camera, With<ThumbnailStudioCamera>>,
|
||||||
|
mut camera_targets: Query<&mut RenderTarget, With<ThumbnailStudioCamera>>,
|
||||||
mut camera_transforms: Query<&mut Transform, With<ThumbnailStudioCamera>>,
|
mut camera_transforms: Query<&mut Transform, With<ThumbnailStudioCamera>>,
|
||||||
children: Query<&Children>,
|
children: Query<&Children>,
|
||||||
transforms: Query<&GlobalTransform>,
|
transforms: Query<&GlobalTransform>,
|
||||||
@ -240,13 +239,14 @@ fn process_thumbnail_studio(
|
|||||||
if active.wait_frames >= LOAD_TIMEOUT_FRAMES {
|
if active.wait_frames >= LOAD_TIMEOUT_FRAMES {
|
||||||
warn!(
|
warn!(
|
||||||
"Model thumbnail timed out framing {} (meshes may still be loading)",
|
"Model thumbnail timed out framing {} (meshes may still be loading)",
|
||||||
active.model_path
|
active.label
|
||||||
);
|
);
|
||||||
fail_active_thumbnail(
|
fail_active_thumbnail(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&mut studio,
|
&mut studio,
|
||||||
&mut cache,
|
&mut cache,
|
||||||
&mut cameras,
|
&mut cameras,
|
||||||
|
&mut camera_targets,
|
||||||
active,
|
active,
|
||||||
"load timeout",
|
"load timeout",
|
||||||
true,
|
true,
|
||||||
@ -277,10 +277,16 @@ fn process_thumbnail_studio(
|
|||||||
} else {
|
} else {
|
||||||
cache.complete_studio_thumbnail(
|
cache.complete_studio_thumbnail(
|
||||||
&active.cache_key,
|
&active.cache_key,
|
||||||
studio.render_image.clone(),
|
active.render_image.clone(),
|
||||||
&mut textures,
|
&mut textures,
|
||||||
);
|
);
|
||||||
finish_active_thumbnail(&mut commands, &mut studio, &mut cameras, active);
|
finish_active_thumbnail(
|
||||||
|
&mut commands,
|
||||||
|
&mut studio,
|
||||||
|
&mut cameras,
|
||||||
|
&mut camera_targets,
|
||||||
|
active,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -294,43 +300,45 @@ fn process_thumbnail_studio(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut root_entity = commands.spawn((
|
let render_image = images.add(studio_render_image());
|
||||||
EditorOnly,
|
if let Ok(mut target) = camera_targets.get_mut(studio.camera) {
|
||||||
ThumbnailSceneRoot,
|
*target = RenderTarget::Image(render_image.clone().into());
|
||||||
ThumbnailStudioLayer,
|
}
|
||||||
RenderLayers::layer(THUMBNAIL_LAYER),
|
if let Ok(mut camera) = cameras.get_mut(studio.camera) {
|
||||||
Transform::default(),
|
camera.is_active = false;
|
||||||
));
|
|
||||||
|
|
||||||
if uses_scene_root(&job.model_path) {
|
|
||||||
root_entity.insert(SceneRoot(
|
|
||||||
asset_server.load(model_scene_asset_path(&job.model_path, 0)),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let root = root_entity.id();
|
let root = commands
|
||||||
|
.spawn((
|
||||||
|
EditorOnly,
|
||||||
|
ThumbnailSceneRoot,
|
||||||
|
ThumbnailStudioLayer,
|
||||||
|
RenderLayers::layer(THUMBNAIL_LAYER),
|
||||||
|
Transform::default(),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
let content_spawned = if let Some(source) = source_for_extension(&job.model_path) {
|
let label = job.source.label().to_string();
|
||||||
source.spawn_preview(
|
let content_spawned = spawn_thumbnail_job_content(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&mut mesh_storage,
|
&mut mesh_storage,
|
||||||
&mut materials,
|
&mut materials,
|
||||||
root,
|
&asset_server,
|
||||||
&job.model_path,
|
root,
|
||||||
)
|
&job.source,
|
||||||
} else {
|
);
|
||||||
uses_scene_root(&job.model_path)
|
|
||||||
};
|
|
||||||
|
|
||||||
if source_for_extension(&job.model_path).is_some() && !content_spawned {
|
if !content_spawned {
|
||||||
fail_active_thumbnail(
|
fail_active_thumbnail(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&mut studio,
|
&mut studio,
|
||||||
&mut cache,
|
&mut cache,
|
||||||
&mut cameras,
|
&mut cameras,
|
||||||
|
&mut camera_targets,
|
||||||
ActiveModelThumbnail {
|
ActiveModelThumbnail {
|
||||||
cache_key: job.cache_key,
|
cache_key: job.cache_key,
|
||||||
model_path: job.model_path,
|
label,
|
||||||
|
render_image,
|
||||||
root,
|
root,
|
||||||
meshes_ready: false,
|
meshes_ready: false,
|
||||||
mesh_warmup_frames: 0,
|
mesh_warmup_frames: 0,
|
||||||
@ -348,7 +356,8 @@ fn process_thumbnail_studio(
|
|||||||
|
|
||||||
studio.active = Some(ActiveModelThumbnail {
|
studio.active = Some(ActiveModelThumbnail {
|
||||||
cache_key: job.cache_key,
|
cache_key: job.cache_key,
|
||||||
model_path: job.model_path,
|
label,
|
||||||
|
render_image,
|
||||||
root,
|
root,
|
||||||
meshes_ready: false,
|
meshes_ready: false,
|
||||||
mesh_warmup_frames: 0,
|
mesh_warmup_frames: 0,
|
||||||
@ -360,13 +369,127 @@ fn process_thumbnail_studio(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn_thumbnail_job_content(
|
||||||
|
commands: &mut Commands,
|
||||||
|
mesh_storage: &mut Assets<Mesh>,
|
||||||
|
materials: &mut Assets<StandardMaterial>,
|
||||||
|
asset_server: &AssetServer,
|
||||||
|
root: Entity,
|
||||||
|
source: &ThumbnailJobSource,
|
||||||
|
) -> bool {
|
||||||
|
match source {
|
||||||
|
ThumbnailJobSource::Model { model_path } => {
|
||||||
|
if uses_scene_root(model_path) {
|
||||||
|
commands.entity(root).insert(SceneRoot(
|
||||||
|
asset_server.load(model_scene_asset_path(model_path, 0)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(source) = source_for_extension(model_path) {
|
||||||
|
source.spawn_preview(commands, mesh_storage, materials, root, model_path)
|
||||||
|
} else {
|
||||||
|
uses_scene_root(model_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ThumbnailJobSource::MeshSubAsset {
|
||||||
|
model_path,
|
||||||
|
mesh_label,
|
||||||
|
material_label,
|
||||||
|
} => spawn_mesh_subasset_preview(
|
||||||
|
commands,
|
||||||
|
materials,
|
||||||
|
asset_server,
|
||||||
|
root,
|
||||||
|
model_path,
|
||||||
|
mesh_label,
|
||||||
|
material_label.as_deref(),
|
||||||
|
),
|
||||||
|
ThumbnailJobSource::SourceMaterial {
|
||||||
|
model_path,
|
||||||
|
material_label,
|
||||||
|
} => spawn_material_sphere(
|
||||||
|
commands,
|
||||||
|
mesh_storage,
|
||||||
|
root,
|
||||||
|
asset_server.load(labeled_asset_path(
|
||||||
|
&asset_server_path(model_path),
|
||||||
|
material_label,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
ThumbnailJobSource::MaterialAsset { material, .. } => {
|
||||||
|
let material = materials.add(material_from_desc(asset_server, material));
|
||||||
|
spawn_material_sphere(commands, mesh_storage, root, material)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_mesh_subasset_preview(
|
||||||
|
commands: &mut Commands,
|
||||||
|
materials: &mut Assets<StandardMaterial>,
|
||||||
|
asset_server: &AssetServer,
|
||||||
|
root: Entity,
|
||||||
|
model_path: &str,
|
||||||
|
mesh_label: &str,
|
||||||
|
material_label: Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
let source_path = asset_server_path(model_path);
|
||||||
|
let mesh: Handle<Mesh> = asset_server.load(labeled_asset_path(&source_path, mesh_label));
|
||||||
|
let material = if let Some(material_label) = material_label {
|
||||||
|
asset_server.load(labeled_asset_path(&source_path, material_label))
|
||||||
|
} else {
|
||||||
|
materials.add(StandardMaterial {
|
||||||
|
base_color: Color::srgb(0.72, 0.72, 0.75),
|
||||||
|
perceptual_roughness: 0.65,
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
commands.entity(root).with_children(|parent| {
|
||||||
|
parent.spawn((
|
||||||
|
RenderLayers::layer(THUMBNAIL_LAYER),
|
||||||
|
Mesh3d(mesh),
|
||||||
|
MeshMaterial3d(material),
|
||||||
|
Transform::default(),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_material_sphere(
|
||||||
|
commands: &mut Commands,
|
||||||
|
mesh_storage: &mut Assets<Mesh>,
|
||||||
|
root: Entity,
|
||||||
|
material: Handle<StandardMaterial>,
|
||||||
|
) -> bool {
|
||||||
|
let mesh = mesh_storage.add(Sphere::new(0.65).mesh().uv(48, 24));
|
||||||
|
commands.entity(root).with_children(|parent| {
|
||||||
|
parent.spawn((
|
||||||
|
RenderLayers::layer(THUMBNAIL_LAYER),
|
||||||
|
Mesh3d(mesh),
|
||||||
|
MeshMaterial3d(material),
|
||||||
|
Transform::default(),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn labeled_asset_path(path: &str, label: &str) -> String {
|
||||||
|
format!("{path}#{label}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn studio_render_image() -> Image {
|
||||||
|
Image::new_target_texture(THUMB_SIZE, THUMB_SIZE, TextureFormat::Rgba8UnormSrgb, None)
|
||||||
|
}
|
||||||
|
|
||||||
fn finish_active_thumbnail(
|
fn finish_active_thumbnail(
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
studio: &mut ThumbnailStudio,
|
studio: &mut ThumbnailStudio,
|
||||||
cameras: &mut Query<&mut Camera, With<ThumbnailStudioCamera>>,
|
cameras: &mut Query<&mut Camera, With<ThumbnailStudioCamera>>,
|
||||||
|
camera_targets: &mut Query<&mut RenderTarget, With<ThumbnailStudioCamera>>,
|
||||||
active: ActiveModelThumbnail,
|
active: ActiveModelThumbnail,
|
||||||
) {
|
) {
|
||||||
despawn_thumbnail_root(commands, active.root);
|
despawn_thumbnail_root(commands, active.root);
|
||||||
|
if let Ok(mut target) = camera_targets.get_mut(studio.camera) {
|
||||||
|
*target = RenderTarget::Image(studio.render_image.clone().into());
|
||||||
|
}
|
||||||
deactivate_studio_camera(cameras, studio.camera);
|
deactivate_studio_camera(cameras, studio.camera);
|
||||||
studio.cooldown_frames = COOLDOWN_FRAMES;
|
studio.cooldown_frames = COOLDOWN_FRAMES;
|
||||||
}
|
}
|
||||||
@ -376,12 +499,13 @@ fn fail_active_thumbnail(
|
|||||||
studio: &mut ThumbnailStudio,
|
studio: &mut ThumbnailStudio,
|
||||||
cache: &mut AssetThumbnailCache,
|
cache: &mut AssetThumbnailCache,
|
||||||
cameras: &mut Query<&mut Camera, With<ThumbnailStudioCamera>>,
|
cameras: &mut Query<&mut Camera, With<ThumbnailStudioCamera>>,
|
||||||
|
camera_targets: &mut Query<&mut RenderTarget, With<ThumbnailStudioCamera>>,
|
||||||
active: ActiveModelThumbnail,
|
active: ActiveModelThumbnail,
|
||||||
reason: &str,
|
reason: &str,
|
||||||
retryable: bool,
|
retryable: bool,
|
||||||
) {
|
) {
|
||||||
cache.mark_studio_failed(&active.cache_key, reason, retryable);
|
cache.mark_studio_failed(&active.cache_key, reason, retryable);
|
||||||
finish_active_thumbnail(commands, studio, cameras, active);
|
finish_active_thumbnail(commands, studio, cameras, camera_targets, active);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_thumbnail_studio_layers(
|
fn apply_thumbnail_studio_layers(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -3,8 +3,10 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use shared::MaterialAsset;
|
||||||
|
|
||||||
use crate::assets::{ASSETS_ROOT, BUILTINS_FOLDER};
|
use crate::asset_db::ImportSettings;
|
||||||
|
use crate::assets::{AssetSelection, ASSETS_ROOT, BUILTINS_FOLDER};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub(crate) enum AssetBrowserView {
|
pub(crate) enum AssetBrowserView {
|
||||||
@ -41,6 +43,31 @@ pub struct AssetBrowserUiState {
|
|||||||
pub(crate) recursive: bool,
|
pub(crate) recursive: bool,
|
||||||
pub(crate) show_details: bool,
|
pub(crate) show_details: bool,
|
||||||
pub(crate) expanded_folders: HashSet<String>,
|
pub(crate) expanded_folders: HashSet<String>,
|
||||||
|
pub(crate) expanded_assets: HashSet<String>,
|
||||||
|
pub(crate) pending_delete: Option<AssetDeleteRequest>,
|
||||||
|
pub(crate) import_draft: Option<ImportSettingsDraft>,
|
||||||
|
pub(crate) material_draft: Option<MaterialAssetDraft>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct AssetDeleteRequest {
|
||||||
|
pub(crate) selection: AssetSelection,
|
||||||
|
pub(crate) label: String,
|
||||||
|
pub(crate) files: Vec<String>,
|
||||||
|
pub(crate) warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct ImportSettingsDraft {
|
||||||
|
pub(crate) path: String,
|
||||||
|
pub(crate) settings: ImportSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct MaterialAssetDraft {
|
||||||
|
pub(crate) path: String,
|
||||||
|
pub(crate) asset: MaterialAsset,
|
||||||
|
pub(crate) error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AssetBrowserUiState {
|
impl Default for AssetBrowserUiState {
|
||||||
@ -57,6 +84,10 @@ impl Default for AssetBrowserUiState {
|
|||||||
recursive: false,
|
recursive: false,
|
||||||
show_details: true,
|
show_details: true,
|
||||||
expanded_folders,
|
expanded_folders,
|
||||||
|
expanded_assets: HashSet::new(),
|
||||||
|
pending_delete: None,
|
||||||
|
import_draft: None,
|
||||||
|
material_draft: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,14 @@ use bevy::prelude::*;
|
|||||||
use bevy_egui::{egui, EguiTextureHandle, EguiUserTextures};
|
use bevy_egui::{egui, EguiTextureHandle, EguiUserTextures};
|
||||||
use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
|
use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
|
||||||
use egui_phosphor_icons::icons;
|
use egui_phosphor_icons::icons;
|
||||||
|
use transform_gizmo_bevy::prelude::GizmoOptions;
|
||||||
|
|
||||||
use crate::assets::EditorAssets;
|
use crate::assets::{AssetSelection, AssetSubAssetKind, EditorAssets};
|
||||||
use crate::gizmos::{EditorGizmoMode, EditorGizmoSpace};
|
use crate::gizmos::{EditorGizmoMode, EditorGizmoSpace};
|
||||||
use crate::render_target::ViewportRenderTarget;
|
use crate::render_target::ViewportRenderTarget;
|
||||||
use crate::selection::ViewportClick;
|
use crate::selection::ViewportClick;
|
||||||
use crate::state::PlayPossession;
|
use crate::state::PlayPossession;
|
||||||
|
use crate::viewport::actor_icons::ActorIconSettings;
|
||||||
use crate::viewport::{
|
use crate::viewport::{
|
||||||
snap_translation, viewport_ground_position, EditorViewportMode, ViewportDisplayMode,
|
snap_translation, viewport_ground_position, EditorViewportMode, ViewportDisplayMode,
|
||||||
ViewportSettings,
|
ViewportSettings,
|
||||||
@ -378,6 +380,13 @@ fn scene_options_popover(
|
|||||||
.text("Snap step"),
|
.text("Snap step"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(mut gizmo_options) = world.get_resource_mut::<GizmoOptions>() {
|
||||||
|
ui.separator();
|
||||||
|
ui.add(
|
||||||
|
egui::Slider::new(&mut gizmo_options.visuals.gizmo_size, 40.0..=160.0)
|
||||||
|
.text("Gizmo size"),
|
||||||
|
);
|
||||||
|
}
|
||||||
if let Some(mut mode) = world.get_resource_mut::<EditorViewportMode>() {
|
if let Some(mut mode) = world.get_resource_mut::<EditorViewportMode>() {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label("Shading");
|
ui.label("Shading");
|
||||||
@ -387,6 +396,7 @@ fn scene_options_popover(
|
|||||||
ui.selectable_value(&mut *mode, EditorViewportMode::Collider, "Colliders");
|
ui.selectable_value(&mut *mode, EditorViewportMode::Collider, "Colliders");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
let mut actor_icons_active = false;
|
||||||
if let Some(mut visualizers) =
|
if let Some(mut visualizers) =
|
||||||
world.get_resource_mut::<crate::visualizers::EditorVisualizationSettings>()
|
world.get_resource_mut::<crate::visualizers::EditorVisualizationSettings>()
|
||||||
{
|
{
|
||||||
@ -409,6 +419,7 @@ fn scene_options_popover(
|
|||||||
ui.checkbox(&mut visualizers.show_actor_icon_debug, "Warnings");
|
ui.checkbox(&mut visualizers.show_actor_icon_debug, "Warnings");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
actor_icons_active = visualizers.enabled && visualizers.show_actor_icons;
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.checkbox(&mut visualizers.show_colliders, "Colliders");
|
ui.checkbox(&mut visualizers.show_colliders, "Colliders");
|
||||||
ui.checkbox(&mut visualizers.show_lights, "Lights");
|
ui.checkbox(&mut visualizers.show_lights, "Lights");
|
||||||
@ -421,6 +432,18 @@ fn scene_options_popover(
|
|||||||
ui.checkbox(&mut visualizers.pickable_proxies, "Selectable proxies");
|
ui.checkbox(&mut visualizers.pickable_proxies, "Selectable proxies");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if actor_icons_active {
|
||||||
|
if let Some(mut icon_settings) = world.get_resource_mut::<ActorIconSettings>() {
|
||||||
|
ui.add(
|
||||||
|
egui::Slider::new(
|
||||||
|
&mut icon_settings.size_pixels,
|
||||||
|
ActorIconSettings::MIN_SIZE_PIXELS
|
||||||
|
..=ActorIconSettings::MAX_SIZE_PIXELS,
|
||||||
|
)
|
||||||
|
.text("Icon size"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Save bookmark").clicked() {
|
if ui.button("Save bookmark").clicked() {
|
||||||
viewport_save_bookmark(world);
|
viewport_save_bookmark(world);
|
||||||
@ -502,12 +525,53 @@ fn viewport_asset_drop_ui(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let asset = {
|
let selection = {
|
||||||
let assets = world.resource::<EditorAssets>();
|
let assets = world.resource::<EditorAssets>();
|
||||||
assets.dragging_asset().cloned()
|
assets.dragging_selection().cloned()
|
||||||
};
|
};
|
||||||
world.resource_mut::<EditorAssets>().clear_drag();
|
world.resource_mut::<EditorAssets>().clear_drag();
|
||||||
|
|
||||||
|
let Some(selection) = selection else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let placement = viewport_ground_position(world, pointer_pos.unwrap(), viewport_rect)
|
||||||
|
.map(|pos| snap_translation(pos, world.resource::<ViewportSettings>()))
|
||||||
|
.unwrap_or(Vec3::ZERO);
|
||||||
|
if matches!(
|
||||||
|
selection,
|
||||||
|
AssetSelection::SubAsset {
|
||||||
|
kind: AssetSubAssetKind::Mesh,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
crate::assets::spawn_subasset_at(world, &selection, placement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let AssetSelection::SubAsset {
|
||||||
|
kind: AssetSubAssetKind::Texture,
|
||||||
|
source_path: Some(path),
|
||||||
|
label,
|
||||||
|
..
|
||||||
|
} = &selection
|
||||||
|
{
|
||||||
|
let asset = crate::assets::EditorAsset {
|
||||||
|
label: label.clone(),
|
||||||
|
path: Some(path.clone()),
|
||||||
|
folder_path: std::path::Path::new(path)
|
||||||
|
.parent()
|
||||||
|
.map(|parent| parent.to_string_lossy().replace('\\', "/"))
|
||||||
|
.unwrap_or_else(|| crate::assets::ASSETS_ROOT.to_string()),
|
||||||
|
kind: crate::assets::EditorAssetKind::Texture,
|
||||||
|
};
|
||||||
|
crate::ui::helpers::apply_texture_to_selection(world, &asset, selected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let asset = {
|
||||||
|
let assets = world.resource::<EditorAssets>();
|
||||||
|
assets.asset_for_selection(&selection).cloned()
|
||||||
|
};
|
||||||
let Some(asset) = asset else {
|
let Some(asset) = asset else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -523,8 +587,5 @@ fn viewport_asset_drop_ui(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let placement = viewport_ground_position(world, pointer_pos.unwrap(), viewport_rect)
|
|
||||||
.map(|pos| snap_translation(pos, world.resource::<ViewportSettings>()))
|
|
||||||
.unwrap_or(Vec3::ZERO);
|
|
||||||
crate::assets::spawn_asset_at(world, &asset, placement);
|
crate::assets::spawn_asset_at(world, &asset, placement);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,17 @@
|
|||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use bevy::asset::{load_internal_binary_asset, uuid_handle, RenderAssetUsages};
|
use bevy::asset::{
|
||||||
|
load_internal_asset, load_internal_binary_asset, uuid_handle, RenderAssetUsages,
|
||||||
|
};
|
||||||
use bevy::camera::visibility::NoFrustumCulling;
|
use bevy::camera::visibility::NoFrustumCulling;
|
||||||
use bevy::image::{CompressedImageFormats, ImageSampler, ImageType};
|
use bevy::image::{CompressedImageFormats, ImageSampler, ImageType};
|
||||||
|
use bevy::pbr::{Material, MaterialPlugin};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use bevy::render::render_resource::{
|
||||||
|
AsBindGroup, BlendState, CompareFunction, RenderPipelineDescriptor, ShaderType,
|
||||||
|
};
|
||||||
|
use bevy::shader::ShaderRef;
|
||||||
use shared::{
|
use shared::{
|
||||||
ActorKind, AuthoringLightKind, ColliderDesc, LevelObject, LightDesc, ModelRef, ObjectiveMarker,
|
ActorKind, AuthoringLightKind, ColliderDesc, LevelObject, LightDesc, ModelRef, ObjectiveMarker,
|
||||||
PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive,
|
PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive,
|
||||||
@ -19,11 +26,13 @@ use crate::ui::UiState;
|
|||||||
use crate::viewport::ViewportDisplayMode;
|
use crate::viewport::ViewportDisplayMode;
|
||||||
use crate::visualizers::EditorVisualizationSettings;
|
use crate::visualizers::EditorVisualizationSettings;
|
||||||
|
|
||||||
const ICON_TARGET_PIXELS: f32 = 30.0;
|
const ICON_MIN_WORLD_SIZE: f32 = 0.001;
|
||||||
const ICON_MIN_WORLD_SIZE: f32 = 0.2;
|
const ICON_MAX_WORLD_SIZE: f32 = 10_000.0;
|
||||||
const ICON_MAX_WORLD_SIZE: f32 = 3.0;
|
const ICON_BRIGHTNESS: f32 = 1.6;
|
||||||
const DEFAULT_PERSPECTIVE_FOV: f32 = std::f32::consts::FRAC_PI_4;
|
const DEFAULT_PERSPECTIVE_FOV: f32 = std::f32::consts::FRAC_PI_4;
|
||||||
|
|
||||||
|
const ACTOR_ICON_OVERLAY_SHADER: Handle<Shader> =
|
||||||
|
uuid_handle!("2edfe544-3ed1-4e13-a880-f56b09804b52");
|
||||||
const ICON_01_ACTOR_EMPTY: Handle<Image> = uuid_handle!("d87c7796-b219-4011-8305-4db55422f1c7");
|
const ICON_01_ACTOR_EMPTY: Handle<Image> = uuid_handle!("d87c7796-b219-4011-8305-4db55422f1c7");
|
||||||
const ICON_02_ACTOR_GENERIC: Handle<Image> = uuid_handle!("c9a1a84f-4dcd-4a2b-a5b6-c365dc7d6795");
|
const ICON_02_ACTOR_GENERIC: Handle<Image> = uuid_handle!("c9a1a84f-4dcd-4a2b-a5b6-c365dc7d6795");
|
||||||
const ICON_03_ACTOR_MISSING: Handle<Image> = uuid_handle!("cca7cdf9-0c7d-4546-9bb9-1176e49a635c");
|
const ICON_03_ACTOR_MISSING: Handle<Image> = uuid_handle!("cca7cdf9-0c7d-4546-9bb9-1176e49a635c");
|
||||||
@ -69,6 +78,85 @@ pub struct ActorIconProxy {
|
|||||||
pub target: Entity,
|
pub target: Entity,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Debug, Clone)]
|
||||||
|
pub struct ActorIconSettings {
|
||||||
|
pub size_pixels: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActorIconSettings {
|
||||||
|
pub const DEFAULT_SIZE_PIXELS: f32 = 34.0;
|
||||||
|
pub const MIN_SIZE_PIXELS: f32 = 16.0;
|
||||||
|
pub const MAX_SIZE_PIXELS: f32 = 96.0;
|
||||||
|
|
||||||
|
pub fn clamped_size_pixels(&self) -> f32 {
|
||||||
|
self.size_pixels
|
||||||
|
.clamp(Self::MIN_SIZE_PIXELS, Self::MAX_SIZE_PIXELS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ActorIconSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
size_pixels: Self::DEFAULT_SIZE_PIXELS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, ShaderType)]
|
||||||
|
struct ActorIconOverlayUniform {
|
||||||
|
brightness: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Asset, TypePath, AsBindGroup, Clone)]
|
||||||
|
struct ActorIconOverlayMaterial {
|
||||||
|
#[texture(0)]
|
||||||
|
#[sampler(1)]
|
||||||
|
texture: Handle<Image>,
|
||||||
|
#[uniform(2)]
|
||||||
|
uniform: ActorIconOverlayUniform,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Material for ActorIconOverlayMaterial {
|
||||||
|
fn fragment_shader() -> ShaderRef {
|
||||||
|
ACTOR_ICON_OVERLAY_SHADER.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha_mode(&self) -> AlphaMode {
|
||||||
|
AlphaMode::Blend
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable_prepass() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable_shadows() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn specialize(
|
||||||
|
_pipeline: &bevy::pbr::MaterialPipeline,
|
||||||
|
descriptor: &mut RenderPipelineDescriptor,
|
||||||
|
_layout: &bevy::mesh::MeshVertexBufferLayoutRef,
|
||||||
|
_key: bevy::pbr::MaterialPipelineKey<Self>,
|
||||||
|
) -> Result<(), bevy::render::render_resource::SpecializedMeshPipelineError> {
|
||||||
|
descriptor.primitive.cull_mode = None;
|
||||||
|
if let Some(depth) = &mut descriptor.depth_stencil {
|
||||||
|
depth.depth_write_enabled = false;
|
||||||
|
depth.depth_compare = CompareFunction::Always;
|
||||||
|
}
|
||||||
|
if let Some(fragment) = &mut descriptor.fragment {
|
||||||
|
if let Some(target) = fragment
|
||||||
|
.targets
|
||||||
|
.first_mut()
|
||||||
|
.and_then(|target| target.as_mut())
|
||||||
|
{
|
||||||
|
target.blend = Some(BlendState::ALPHA_BLENDING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
enum ActorIconImage {
|
enum ActorIconImage {
|
||||||
Empty,
|
Empty,
|
||||||
@ -126,7 +214,7 @@ struct ActorIconComponents {
|
|||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct ActorIconAssets {
|
struct ActorIconAssets {
|
||||||
mesh: Handle<Mesh>,
|
mesh: Handle<Mesh>,
|
||||||
materials: HashMap<ActorIconImage, Handle<StandardMaterial>>,
|
materials: HashMap<ActorIconImage, Handle<ActorIconOverlayMaterial>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ActorIconsPlugin;
|
pub struct ActorIconsPlugin;
|
||||||
@ -134,7 +222,16 @@ pub struct ActorIconsPlugin;
|
|||||||
impl Plugin for ActorIconsPlugin {
|
impl Plugin for ActorIconsPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
register_actor_icon_images(app);
|
register_actor_icon_images(app);
|
||||||
app.add_systems(Startup, init_actor_icon_assets)
|
load_internal_asset!(
|
||||||
|
app,
|
||||||
|
ACTOR_ICON_OVERLAY_SHADER,
|
||||||
|
"../../assets/shaders/actor_icon_overlay.wgsl",
|
||||||
|
Shader::from_wgsl
|
||||||
|
);
|
||||||
|
|
||||||
|
app.init_resource::<ActorIconSettings>()
|
||||||
|
.add_plugins(MaterialPlugin::<ActorIconOverlayMaterial>::default())
|
||||||
|
.add_systems(Startup, init_actor_icon_assets)
|
||||||
.add_systems(Update, sync_actor_icons.run_if(scene_tools_active));
|
.add_systems(Update, sync_actor_icons.run_if(scene_tools_active));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,7 +301,7 @@ fn register_actor_icon_images(app: &mut App) {
|
|||||||
fn init_actor_icon_assets(
|
fn init_actor_icon_assets(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<ActorIconOverlayMaterial>>,
|
||||||
) {
|
) {
|
||||||
let mesh = meshes.add(Rectangle::new(1.0, 1.0));
|
let mesh = meshes.add(Rectangle::new(1.0, 1.0));
|
||||||
let mut actor_materials = HashMap::new();
|
let mut actor_materials = HashMap::new();
|
||||||
@ -229,14 +326,11 @@ fn init_actor_icon_assets(
|
|||||||
] {
|
] {
|
||||||
actor_materials.insert(
|
actor_materials.insert(
|
||||||
image,
|
image,
|
||||||
materials.add(StandardMaterial {
|
materials.add(ActorIconOverlayMaterial {
|
||||||
base_color_texture: Some(texture.clone()),
|
texture: texture.clone(),
|
||||||
alpha_mode: AlphaMode::Blend,
|
uniform: ActorIconOverlayUniform {
|
||||||
unlit: true,
|
brightness: ICON_BRIGHTNESS,
|
||||||
cull_mode: None,
|
},
|
||||||
fog_enabled: false,
|
|
||||||
depth_bias: -40.0,
|
|
||||||
..default()
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -251,13 +345,14 @@ fn init_actor_icon_assets(
|
|||||||
fn sync_actor_icons(
|
fn sync_actor_icons(
|
||||||
display: Res<ViewportDisplayMode>,
|
display: Res<ViewportDisplayMode>,
|
||||||
settings: Res<EditorVisualizationSettings>,
|
settings: Res<EditorVisualizationSettings>,
|
||||||
|
icon_settings: Res<ActorIconSettings>,
|
||||||
ui_state: Res<UiState>,
|
ui_state: Res<UiState>,
|
||||||
assets: Res<ActorIconAssets>,
|
assets: Res<ActorIconAssets>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut icons: Query<(
|
mut icons: Query<(
|
||||||
Entity,
|
Entity,
|
||||||
&ActorIconProxy,
|
&ActorIconProxy,
|
||||||
&MeshMaterial3d<StandardMaterial>,
|
&MeshMaterial3d<ActorIconOverlayMaterial>,
|
||||||
&mut Transform,
|
&mut Transform,
|
||||||
)>,
|
)>,
|
||||||
camera_query: Query<(&Camera, &GlobalTransform, Option<&Projection>), With<EditorCamera>>,
|
camera_query: Query<(&Camera, &GlobalTransform, Option<&Projection>), With<EditorCamera>>,
|
||||||
@ -325,7 +420,12 @@ fn sync_actor_icons(
|
|||||||
if !icon_category_enabled(&settings, spec.category) {
|
if !icon_category_enabled(&settings, spec.category) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let transform = desired_icon_transform(global, camera, ui_state.viewport_rect.height());
|
let transform = desired_icon_transform(
|
||||||
|
global,
|
||||||
|
camera,
|
||||||
|
ui_state.viewport_rect.height(),
|
||||||
|
icon_settings.clamped_size_pixels(),
|
||||||
|
);
|
||||||
desired.insert(target, (spec.image, transform));
|
desired.insert(target, (spec.image, transform));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -390,7 +490,12 @@ fn sync_actor_icons(
|
|||||||
let Some(material) = assets.materials.get(&spec.image).cloned() else {
|
let Some(material) = assets.materials.get(&spec.image).cloned() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let transform = desired_icon_transform(global, camera, ui_state.viewport_rect.height());
|
let transform = desired_icon_transform(
|
||||||
|
global,
|
||||||
|
camera,
|
||||||
|
ui_state.viewport_rect.height(),
|
||||||
|
icon_settings.clamped_size_pixels(),
|
||||||
|
);
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
EditorOnly,
|
EditorOnly,
|
||||||
RaytracingExcluded,
|
RaytracingExcluded,
|
||||||
@ -419,6 +524,7 @@ fn desired_icon_transform(
|
|||||||
parent_global: &GlobalTransform,
|
parent_global: &GlobalTransform,
|
||||||
camera: Option<(&Camera, &GlobalTransform, Option<&Projection>)>,
|
camera: Option<(&Camera, &GlobalTransform, Option<&Projection>)>,
|
||||||
viewport_height: f32,
|
viewport_height: f32,
|
||||||
|
target_pixels: f32,
|
||||||
) -> Transform {
|
) -> Transform {
|
||||||
let translation = parent_global.translation();
|
let translation = parent_global.translation();
|
||||||
let Some((_, camera_global, projection)) = camera else {
|
let Some((_, camera_global, projection)) = camera else {
|
||||||
@ -429,6 +535,7 @@ fn desired_icon_transform(
|
|||||||
camera_global,
|
camera_global,
|
||||||
projection,
|
projection,
|
||||||
viewport_height.max(1.0),
|
viewport_height.max(1.0),
|
||||||
|
target_pixels,
|
||||||
);
|
);
|
||||||
let desired_global = Transform {
|
let desired_global = Transform {
|
||||||
translation,
|
translation,
|
||||||
@ -444,18 +551,20 @@ fn icon_world_size(
|
|||||||
camera_global: &GlobalTransform,
|
camera_global: &GlobalTransform,
|
||||||
projection: Option<&Projection>,
|
projection: Option<&Projection>,
|
||||||
viewport_height: f32,
|
viewport_height: f32,
|
||||||
|
target_pixels: f32,
|
||||||
) -> f32 {
|
) -> f32 {
|
||||||
let camera_position = camera_global.translation();
|
let pixels_to_view = target_pixels / viewport_height;
|
||||||
let pixels_to_view = ICON_TARGET_PIXELS / viewport_height;
|
|
||||||
let world_height = match projection {
|
let world_height = match projection {
|
||||||
Some(Projection::Perspective(projection)) => {
|
Some(Projection::Perspective(projection)) => {
|
||||||
let distance = camera_position.distance(target).max(0.1);
|
let target_view = camera_global.affine().inverse().transform_point3(target);
|
||||||
2.0 * distance * (projection.fov * 0.5).tan()
|
let depth = (-target_view.z).abs().max(0.1);
|
||||||
|
2.0 * depth * (projection.fov * 0.5).tan()
|
||||||
}
|
}
|
||||||
Some(Projection::Orthographic(projection)) => projection.area.height().abs().max(0.1),
|
Some(Projection::Orthographic(projection)) => projection.area.height().abs().max(0.1),
|
||||||
_ => {
|
_ => {
|
||||||
let distance = camera_position.distance(target).max(0.1);
|
let target_view = camera_global.affine().inverse().transform_point3(target);
|
||||||
2.0 * distance * (DEFAULT_PERSPECTIVE_FOV * 0.5).tan()
|
let depth = (-target_view.z).abs().max(0.1);
|
||||||
|
2.0 * depth * (DEFAULT_PERSPECTIVE_FOV * 0.5).tan()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
(world_height * pixels_to_view).clamp(ICON_MIN_WORLD_SIZE, ICON_MAX_WORLD_SIZE)
|
(world_height * pixels_to_view).clamp(ICON_MIN_WORLD_SIZE, ICON_MAX_WORLD_SIZE)
|
||||||
@ -653,4 +762,29 @@ mod tests {
|
|||||||
assert_eq!(spec.image, ActorIconImage::PhysicsCollider);
|
assert_eq!(spec.image, ActorIconImage::PhysicsCollider);
|
||||||
assert_eq!(spec.category, ActorIconCategory::Physics);
|
assert_eq!(spec.category, ActorIconCategory::Physics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perspective_icon_size_uses_view_depth() {
|
||||||
|
let camera = GlobalTransform::IDENTITY;
|
||||||
|
let projection = Projection::Perspective(PerspectiveProjection {
|
||||||
|
fov: DEFAULT_PERSPECTIVE_FOV,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
let centered = icon_world_size(
|
||||||
|
Vec3::new(0.0, 0.0, -12.0),
|
||||||
|
&camera,
|
||||||
|
Some(&projection),
|
||||||
|
900.0,
|
||||||
|
ActorIconSettings::DEFAULT_SIZE_PIXELS,
|
||||||
|
);
|
||||||
|
let offset = icon_world_size(
|
||||||
|
Vec3::new(8.0, 0.0, -12.0),
|
||||||
|
&camera,
|
||||||
|
Some(&projection),
|
||||||
|
900.0,
|
||||||
|
ActorIconSettings::DEFAULT_SIZE_PIXELS,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(centered, offset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -306,13 +306,20 @@ fn collect_viewport_pick_hits(
|
|||||||
|
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
let mut ordered = Vec::new();
|
let mut ordered = Vec::new();
|
||||||
for (entity, _) in hits {
|
for icon_priority in [true, false] {
|
||||||
let Some(target) = pick_target_for_entity(*entity, level_objects, proxies, icon_proxies)
|
for (entity, _) in hits {
|
||||||
else {
|
let is_icon = icon_proxies.get(*entity).is_ok();
|
||||||
continue;
|
if is_icon != icon_priority {
|
||||||
};
|
continue;
|
||||||
if seen.insert(target) {
|
}
|
||||||
ordered.push(target);
|
let Some(target) =
|
||||||
|
pick_target_for_entity(*entity, level_objects, proxies, icon_proxies)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if seen.insert(target) {
|
||||||
|
ordered.push(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ordered
|
ordered
|
||||||
|
|||||||
@ -17,7 +17,8 @@ use bevy::ecs::system::SystemState;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use lights::{hydrate_lights, reconcile_missing_runtime_lights};
|
use lights::{hydrate_lights, reconcile_missing_runtime_lights};
|
||||||
use materials::{hydrate_materials, material_from_desc};
|
use materials::hydrate_materials;
|
||||||
|
pub use materials::material_from_desc;
|
||||||
use models::hydrate_models;
|
use models::hydrate_models;
|
||||||
use physics::hydrate_physics;
|
use physics::hydrate_physics;
|
||||||
use prefabs::hydrate_prefabs;
|
use prefabs::hydrate_prefabs;
|
||||||
|
|||||||
@ -14,8 +14,8 @@ mod rendering_profile_asset;
|
|||||||
pub use actor::{infer_actor_kind, validate_actor, ActorValidationError};
|
pub use actor::{infer_actor_kind, validate_actor, ActorValidationError};
|
||||||
pub use components::*;
|
pub use components::*;
|
||||||
pub use hydration::{
|
pub use hydration::{
|
||||||
cascade_config_from_rendering, flush_level_object_hydration, strip_hydrated,
|
cascade_config_from_rendering, flush_level_object_hydration, material_from_desc,
|
||||||
strip_hydrated_entity, HydrationPlugin,
|
strip_hydrated, strip_hydrated_entity, HydrationPlugin,
|
||||||
};
|
};
|
||||||
pub use material_asset::{
|
pub use material_asset::{
|
||||||
MaterialAsset, ShaderPropertyDesc, ShaderPropertyType, ShaderSchemaAsset,
|
MaterialAsset, ShaderPropertyDesc, ShaderPropertyType, ShaderSchemaAsset,
|
||||||
|
|||||||
@ -33,6 +33,7 @@ Immutable-style log of significant decisions. Add a new numbered ADR when changi
|
|||||||
| [0016](adr/0016-unified-rendering-contract.md) | Requested/effective render stack, Solari eligibility, emissive materials |
|
| [0016](adr/0016-unified-rendering-contract.md) | Requested/effective render stack, Solari eligibility, emissive materials |
|
||||||
| [0017](adr/0017-normalized-static-mesh-assets.md) | Normalized static mesh assets and renderer placement |
|
| [0017](adr/0017-normalized-static-mesh-assets.md) | Normalized static mesh assets and renderer placement |
|
||||||
| [0018](adr/0018-componentized-actor-inspector-and-materials.md) | Componentized actor inspector, imported asset refs, collider split, material overrides |
|
| [0018](adr/0018-componentized-actor-inspector-and-materials.md) | Componentized actor inspector, imported asset refs, collider split, material overrides |
|
||||||
|
| [0019](adr/0019-local-bevy-render-timeout-patch.md) | Local `bevy_render` patch for transient Linux swapchain timeouts |
|
||||||
|
|
||||||
## Editor framework
|
## Editor framework
|
||||||
|
|
||||||
|
|||||||
28
docs/adr/0019-local-bevy-render-timeout-patch.md
Normal file
28
docs/adr/0019-local-bevy-render-timeout-patch.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
Context
|
||||||
|
=======
|
||||||
|
|
||||||
|
The editor targets Linux desktop sessions where transient swapchain acquire
|
||||||
|
timeouts can occur during heavy GPU work, compositor stalls, or driver hiccups.
|
||||||
|
In upstream `bevy_render` 0.18.1, `prepare_windows` treats
|
||||||
|
`wgpu::SurfaceError::Timeout` as a fatal path. That can crash the editor even
|
||||||
|
though the next frame can often recover.
|
||||||
|
|
||||||
|
Decision
|
||||||
|
========
|
||||||
|
|
||||||
|
Patch `bevy_render` locally through `[patch.crates-io]` and vendor the scoped
|
||||||
|
crate copy under `third_party/bevy_render`. The patch is limited to transient
|
||||||
|
surface acquire timeouts in `prepare_windows`: log the timeout and skip the
|
||||||
|
frame instead of panicking. Other surface errors remain on their existing paths.
|
||||||
|
|
||||||
|
Consequences
|
||||||
|
============
|
||||||
|
|
||||||
|
The editor is more tolerant of transient Linux swapchain stalls while preserving
|
||||||
|
the current Bevy API surface for the rest of the workspace.
|
||||||
|
|
||||||
|
The vendor patch must be reviewed during every Bevy upgrade and removed once
|
||||||
|
upstream behavior is sufficient. Keep the diff narrow; do not use the vendored
|
||||||
|
crate for unrelated render changes.
|
||||||
@ -37,7 +37,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 and model thumbnails (`AssetThumbnailCache::request_model`), and a compact details pane; narrow docks prioritize content, wrap toolbar/footer controls, 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 and asset content panels scroll. **Built-ins** holds spawnable primitives and lights. **Materials** folder scans `assets/materials/*.ron`; **Shaders** holds shader schema RON files. glTF/GLB/FBX details expose static asset vs scene-instance placement, hierarchy mode, material policy, collider generation, and the generated static mesh artifact path.
|
- **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/`.
|
||||||
- **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.
|
||||||
- **Collider split** — imported mesh collision lives in a separate `ColliderDesc::StaticMesh` plus optional `RigidBodyDesc`; renderer slots own only render visibility, shadows, mesh, and material references.
|
- **Collider split** — imported mesh collision lives in a separate `ColliderDesc::StaticMesh` plus optional `RigidBodyDesc`; renderer slots own only render visibility, shadows, mesh, and material references.
|
||||||
- **Material assets** — `MaterialAsset` RON (`label` + `MaterialDesc`) is shader-schema aware. Actor material edits write `MaterialOverride` entries keyed by renderer slot ID; content-browser asset inspectors are the path for editing shared material assets.
|
- **Material assets** — `MaterialAsset` RON (`label` + `MaterialDesc`) is shader-schema aware. Actor material edits write `MaterialOverride` entries keyed by renderer slot ID; content-browser asset inspectors are the path for editing shared material assets.
|
||||||
@ -50,11 +50,11 @@ Docs for the in-process egui editor (`crates/editor/`). Update this index when a
|
|||||||
- **Inspector** shows a responsive header, **Transform**, registered authoring component cards, and a pinned bottom Add Component footer (`actor_inspector`); only the component body scrolls. Rows wrap or stack inside narrow docks instead of forcing the panel wider, and legacy authoring sections use the same component-card shell. Component card carets collapse/expand for the current editor session, status dots toggle persisted active state, and triple-dot menus provide reset, copy/paste values, move, remove, and documentation slots. Inactive authoring components remain in the scene but are skipped by hydration/runtime queries, stripping their generated meshes, lights, physics, or post-process effects. `StaticMeshRenderer` uses concept-style slot cards with model thumbnails, imported mesh/material selectors, visibility, shadow flags, and per-actor material overrides. Empty actor material refs display the inherited source material from the generated static mesh artifact as **Source default**; selector **Browse** assigns explicit normalized imported sub-assets, **Locate** focuses the owning asset in the Asset Browser, and **Clear** returns optional material slots to the inherited source default. `ColliderDesc` and `RigidBodyDesc` are separate component cards. Game crates add sections via `ActorInspectorSection` + `game::editor_ext::actor_inspector_section_ids()`.
|
- **Inspector** shows a responsive header, **Transform**, registered authoring component cards, and a pinned bottom Add Component footer (`actor_inspector`); only the component body scrolls. Rows wrap or stack inside narrow docks instead of forcing the panel wider, and legacy authoring sections use the same component-card shell. Component card carets collapse/expand for the current editor session, status dots toggle persisted active state, and triple-dot menus provide reset, copy/paste values, move, remove, and documentation slots. Inactive authoring components remain in the scene but are skipped by hydration/runtime queries, stripping their generated meshes, lights, physics, or post-process effects. `StaticMeshRenderer` uses concept-style slot cards with model thumbnails, imported mesh/material selectors, visibility, shadow flags, and per-actor material overrides. Empty actor material refs display the inherited source material from the generated static mesh artifact as **Source default**; selector **Browse** assigns explicit normalized imported sub-assets, **Locate** focuses the owning asset in the Asset Browser, and **Clear** returns optional material slots to the inherited source default. `ColliderDesc` and `RigidBodyDesc` are separate component cards. Game crates add sections via `ActorInspectorSection` + `game::editor_ext::actor_inspector_section_ids()`.
|
||||||
- **Command palette** (`Ctrl+P`): `scene.reset_lighting`, `rendering.create_volume`, `rendering.select_volumes_at_camera`, `rendering.focus_active_volumes`, `selection.group`, `selection.focus`, and play commands.
|
- **Command palette** (`Ctrl+P`): `scene.reset_lighting`, `rendering.create_volume`, `rendering.select_volumes_at_camera`, `rendering.focus_active_volumes`, `selection.group`, `selection.focus`, and play commands.
|
||||||
- **ActorKind** is required on saved level objects (schema v2). Save strips hydrated ECS before writing `.scn.ron`.
|
- **ActorKind** is required on saved level objects (schema v2). Save strips hydrated ECS before writing `.scn.ron`.
|
||||||
- **Scene visualizers** expose actor root icons, colliders, lights, player spawns, prefab/model anchors, Project Sun, and runtime player/camera markers without changing saved scene data.
|
- **Scene visualizers** expose actor root icons, colliders, lights, player spawns, prefab/model anchors, Project Sun, and runtime player/camera markers without changing saved scene data. Viewport options include actor icon size and transform gizmo size sliders.
|
||||||
- **Clean game-view overlay** (`G`) hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxies without changing camera ownership; `Ctrl+G` toggles grid.
|
- **Clean game-view overlay** (`G`) hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxies without changing camera ownership; `Ctrl+G` toggles grid.
|
||||||
- **Viewport shading** (toolbar): **Lit** (default WYSIWYG), **Unlit** (base-color debug), **Colliders** (hide meshes, emphasize collider gizmos). Light gizmos show range spheres, spot cones, and directional sun arrows. A **mode badge** (bottom-right of the viewport) shows the active mode; Collider mode displays a warning banner.
|
- **Viewport shading** (toolbar): **Lit** (default WYSIWYG), **Unlit** (base-color debug), **Colliders** (hide meshes, emphasize collider gizmos). Light gizmos show range spheres, spot cones, and directional sun arrows. A **mode badge** (bottom-right of the viewport) shows the active mode; Collider mode displays a warning banner.
|
||||||
- **Hierarchy** sort modes (**Manual**, **Name**, **Type**) use stable tie-breakers so duplicate names (e.g. several `Pillar` rows) do not reorder on every click. Drag-and-drop reparenting disables panel scroll-to-drag; drop indicators appear only while dragging.
|
- **Hierarchy** sort modes (**Manual**, **Name**, **Type**) use stable tie-breakers so duplicate names (e.g. several `Pillar` rows) do not reorder on every click. Drag-and-drop reparenting disables panel scroll-to-drag; drop indicators appear only while dragging.
|
||||||
- **Viewport picking**: click selects nearest object, actor root icon, or visualizer hit; **Tab** cycles overlapping targets at the last click (selection pin deferred).
|
- **Viewport picking**: click selects nearest object, actor root icon, or visualizer hit, with actor icons preferred when clicked over meshes; **Tab** cycles overlapping targets at the last click (selection pin deferred).
|
||||||
- **Settings data** lives in `crates/settings`; **settings UI** lives in `settings_ui.rs`.
|
- **Settings data** lives in `crates/settings`; **settings UI** lives in `settings_ui.rs`.
|
||||||
|
|
||||||
When implementing or changing any of the above, update this file and the root README in the same change.
|
When implementing or changing any of the above, update this file and the root README in the same change.
|
||||||
|
|||||||
@ -76,13 +76,13 @@ The viewport uses **render-to-texture**: the active camera renders to an offscre
|
|||||||
|
|
||||||
**HDR invariant:** The atmosphere/HDR stack (`Atmosphere`, `Hdr`, etc.) must never render to the primary window swapchain (`Rgba8UnormSrgb`). When the HDR offscreen target is missing, `scene_view` / `play` deactivate cameras; `render_view` skips stack sync until a target exists.
|
**HDR invariant:** The atmosphere/HDR stack (`Atmosphere`, `Hdr`, etc.) must never render to the primary window swapchain (`Rgba8UnormSrgb`). When the HDR offscreen target is missing, `scene_view` / `play` deactivate cameras; `render_view` skips stack sync until a target exists.
|
||||||
|
|
||||||
**Viewport picking:** LMB selection maps the egui panel UV through `scene_view_ray` and raycasts with `MeshRayCast`. Object pick runs in `Last` after `transform_gizmo_bevy` so gizmo drags take priority. `GizmoOptions.viewport_rect` is synced from the viewport panel rect so gizmo hit-testing matches render-to-texture layout. Actor root icon and visualizer proxy hits resolve to their source entity, so clicking an icon or light/spawn/collider marker selects the authored or runtime object, not the helper. In Edit mode, selecting the runtime Player redirects to an authored `PlayerSpawn` (`Player Start`), creating one if needed.
|
**Viewport picking:** LMB selection maps the egui panel UV through `scene_view_ray` and raycasts with `MeshRayCast`. Object pick runs in `Last` after `transform_gizmo_bevy` so gizmo drags take priority. `GizmoOptions.viewport_rect` is synced from the viewport panel rect so gizmo hit-testing matches render-to-texture layout. Actor root icon and visualizer proxy hits resolve to their source entity, so clicking an icon or light/spawn/collider marker selects the authored or runtime object, not the helper. Actor icon hits are ordered ahead of ordinary mesh hits when both are under the cursor. In Edit mode, selecting the runtime Player redirects to an authored `PlayerSpawn` (`Player Start`), creating one if needed.
|
||||||
|
|
||||||
**Selection overlay:** `selection_outline.rs` draws a shared unit-box shell per selected `LevelObject` (bounds from `Primitive`, physics collider, or mesh AABB), scaled to fit. Custom shader (`assets/shaders/selection_outline.wgsl`) uses rim-only fresnel with two cheap depth passes: `Greater` (soft cyan x-ray) and `LessEqual` (amber edge). Sync runs only when selection changes. Editor-only.
|
**Selection overlay:** `selection_outline.rs` draws a shared unit-box shell per selected `LevelObject` (bounds from `Primitive`, physics collider, or mesh AABB), scaled to fit. Custom shader (`assets/shaders/selection_outline.wgsl`) uses rim-only fresnel with two cheap depth passes: `Greater` (soft cyan x-ray) and `LessEqual` (amber edge). Sync runs only when selection changes. Editor-only.
|
||||||
|
|
||||||
**Clean game-view overlay:** `ViewportDisplayMode.clean_game_view` hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxy meshes without changing camera ownership. `G` toggles this mode; `Ctrl+G` toggles the grid.
|
**Clean game-view overlay:** `ViewportDisplayMode.clean_game_view` hides editor chrome, grid, selection outlines, transform gizmos, actor root icons, visualizers, and selectable proxy meshes without changing camera ownership. `G` toggles this mode; `Ctrl+G` toggles the grid.
|
||||||
|
|
||||||
**Actor root icons:** `actor_icons.rs` registers the editor PNG icon set as internal binary image assets and spawns screen-stable, camera-facing editor-only billboards at `LevelObject` roots. The billboards are `RaytracingExcluded`, hidden with clean game-view, filtered from hierarchy and scene serialization, and expose per-category viewport options for actors, meshes, lights, physics, gameplay, volumes, prefab/model anchors, and warnings.
|
**Actor root icons:** `actor_icons.rs` registers the editor PNG icon set as internal binary image assets and spawns screen-stable, camera-facing editor-only billboards at `LevelObject` roots. A custom overlay material draws them unlit with always-pass depth so opaque meshes do not obstruct them. The billboards are `RaytracingExcluded`, hidden with clean game-view, filtered from hierarchy and scene serialization, and expose per-category viewport options for actors, meshes, lights, physics, gameplay, volumes, prefab/model anchors, warnings, and icon size.
|
||||||
|
|
||||||
**Scene visualizers:** `visualizers.rs` draws transient gizmo lines for authoring components without obvious meshes: colliders, lights, `PlayerSpawn`, prefab/model anchors, Project Sun, and runtime player/camera markers. Small editor-only proxy meshes make those markers selectable but are filtered from hierarchy and scene serialization. Player/start markers use the runtime capsule constants from `sim::tuning`.
|
**Scene visualizers:** `visualizers.rs` draws transient gizmo lines for authoring components without obvious meshes: colliders, lights, `PlayerSpawn`, prefab/model anchors, Project Sun, and runtime player/camera markers. Small editor-only proxy meshes make those markers selectable but are filtered from hierarchy and scene serialization. Player/start markers use the runtime capsule constants from `sim::tuning`.
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ The egui layer lives under `crates/editor/src/ui/`:
|
|||||||
| `theme.rs` | Unity-like dark pro palette + `egui_dock` chrome |
|
| `theme.rs` | Unity-like dark pro palette + `egui_dock` chrome |
|
||||||
| `fonts.rs` | Phosphor icon font on `PrimaryEguiContext` |
|
| `fonts.rs` | Phosphor icon font on `PrimaryEguiContext` |
|
||||||
| `widgets.rs` | Icon buttons, tool toggles, menu helpers |
|
| `widgets.rs` | Icon buttons, tool toggles, menu helpers |
|
||||||
| `asset_browser.rs` | Project/file browser with tree, breadcrumb, filters, list/grid modes, details/import settings |
|
| `asset_browser.rs` | Project/file browser with tree, breadcrumb, filters, list/grid modes, embedded model shelves, details/import settings |
|
||||||
| `menu.rs` | Top menu bar with shortcuts |
|
| `menu.rs` | Top menu bar with shortcuts |
|
||||||
| `viewport_chrome.rs` | Unified viewport RTT + overlay toolbars |
|
| `viewport_chrome.rs` | Unified viewport RTT + overlay toolbars |
|
||||||
| `actor_icons.rs` | Screen-stable actor root icons and selectable icon proxies |
|
| `actor_icons.rs` | Screen-stable actor root icons and selectable icon proxies |
|
||||||
@ -146,6 +146,7 @@ On play enter/exit the editor snapshots **player sim state only** (transform, ve
|
|||||||
|
|
||||||
- **Import:** File → Import Assets copies glTF/GLB/FBX into `assets/models/`. FBX imports also copy a sibling `{name}.fbm/` folder when present (embedded textures).
|
- **Import:** File → Import Assets copies glTF/GLB/FBX into `assets/models/`. FBX imports also copy a sibling `{name}.fbm/` folder when present (embedded textures).
|
||||||
- **Processing:** the asset registry generates normalized static mesh manifests under `assets/meshes/generated/`. The manifests store stable part IDs, glTF/FBX mesh/material subasset labels, source metadata, dependencies, and import settings. These artifacts are hidden from the Asset Browser catalog.
|
- **Processing:** the asset registry generates normalized static mesh manifests under `assets/meshes/generated/`. The manifests store stable part IDs, glTF/FBX mesh/material subasset labels, source metadata, dependencies, and import settings. These artifacts are hidden from the Asset Browser catalog.
|
||||||
|
- **Browser subassets:** model rows can expand into a content shelf backed by the generated manifest. Mesh subassets can be selected or dragged into the viewport as `StaticMeshRenderer` actors, material subassets expose source defaults, and texture dependencies can be applied to selected actors without treating glTF and FBX differently. The thumbnail studio renders each generated model/subasset/material preview into its own render target before registering it in the cache so later thumbnails cannot overwrite earlier cache entries.
|
||||||
- **Default placement:** model drag/drop creates `ActorKind::StaticMesh + StaticMeshRenderer`. Renderer slots reference imported `EditorAssetRef` values (`asset_id` + `sub_asset_id`) and hydration resolves those through the generated artifact. `SingleActor` hierarchy mode stores all parts in one renderer; `SourceHierarchy` creates a root with child static mesh actors.
|
- **Default placement:** model drag/drop creates `ActorKind::StaticMesh + StaticMeshRenderer`. Renderer slots reference imported `EditorAssetRef` values (`asset_id` + `sub_asset_id`) and hydration resolves those through the generated artifact. `SingleActor` hierarchy mode stores all parts in one renderer; `SourceHierarchy` creates a root with child static mesh actors.
|
||||||
- **Collision:** when collider generation is enabled, placement adds `RigidBodyDesc` and `ColliderDesc::StaticMesh` using the same imported mesh refs. Renderer slots no longer own collider state.
|
- **Collision:** when collider generation is enabled, placement adds `RigidBodyDesc` and `ColliderDesc::StaticMesh` using the same imported mesh refs. Renderer slots no longer own collider state.
|
||||||
- **Scene-instance placement:** asset details can switch placement to `SceneInstance`; that keeps `ImportedModel + ModelRef`, which hydrates to `SceneRoot`. glTF uses `GltfAssetLabel::Scene`; FBX uses `bevy_ufbx` (`path#SceneN`). `FbxPlugin` is registered in `GamePlugin`.
|
- **Scene-instance placement:** asset details can switch placement to `SceneInstance`; that keeps `ImportedModel + ModelRef`, which hydrates to `SceneRoot`. glTF uses `GltfAssetLabel::Scene`; FBX uses `bevy_ufbx` (`path#SceneN`). `FbxPlugin` is registered in `GamePlugin`.
|
||||||
@ -178,6 +179,8 @@ Shared scene schema lives in `crates/scene` (stamp/migrate/validate on save, loa
|
|||||||
|
|
||||||
The unified viewport renders through an HDR offscreen target (`render_target.rs`). When the panel target is missing (egui reflow) or settings disable HDR, cameras must strip atmosphere/post-FX before targeting the swapchain. `render_view::sync_project_render_view` runs in **PostUpdate** after RTT target assignment and reapplies the project FX stack whenever the active panel target handle changes (startup, resize), not only on settings Apply. Project Settings uses a **draft buffer**; **Apply** commits after render targets resync.
|
The unified viewport renders through an HDR offscreen target (`render_target.rs`). When the panel target is missing (egui reflow) or settings disable HDR, cameras must strip atmosphere/post-FX before targeting the swapchain. `render_view::sync_project_render_view` runs in **PostUpdate** after RTT target assignment and reapplies the project FX stack whenever the active panel target handle changes (startup, resize), not only on settings Apply. Project Settings uses a **draft buffer**; **Apply** commits after render targets resync.
|
||||||
|
|
||||||
|
The workspace patches `bevy_render` under `third_party/bevy_render` so Linux `wgpu::SurfaceError::Timeout` during swapchain texture acquisition logs and skips the frame instead of panicking in `prepare_windows`. Keep this patch scoped to transient surface acquire timeouts and re-evaluate it during Bevy upgrades.
|
||||||
|
|
||||||
## Extensibility (phase 6)
|
## Extensibility (phase 6)
|
||||||
|
|
||||||
`extensibility.rs` provides `EditorCommand`, `EditorPlugin`, and a working command palette (Ctrl+P). Game crates register panel hooks via `game::editor_ext::editor_panel_setups()`; the editor binary wraps them without editing `ui/mod.rs` (ADR 0007, dogfood: FPS Demo panel, Ctrl+Shift+G).
|
`extensibility.rs` provides `EditorCommand`, `EditorPlugin`, and a working command palette (Ctrl+P). Game crates register panel hooks via `game::editor_ext::editor_panel_setups()`; the editor binary wraps them without editing `ui/mod.rs` (ADR 0007, dogfood: FPS Demo panel, Ctrl+Shift+G).
|
||||||
|
|||||||
296
third_party/bevy_render/Cargo.toml
vendored
Normal file
296
third_party/bevy_render/Cargo.toml
vendored
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||||
|
#
|
||||||
|
# When uploading crates to the registry Cargo will automatically
|
||||||
|
# "normalize" Cargo.toml files for maximal compatibility
|
||||||
|
# with all versions of Cargo and also rewrite `path` dependencies
|
||||||
|
# to registry (e.g., crates.io) dependencies.
|
||||||
|
#
|
||||||
|
# If you are reading this file be aware that the original Cargo.toml
|
||||||
|
# will likely look very different (and much more reasonable).
|
||||||
|
# See Cargo.toml.orig for the original contents.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
edition = "2024"
|
||||||
|
name = "bevy_render"
|
||||||
|
version = "0.18.1"
|
||||||
|
build = false
|
||||||
|
autolib = false
|
||||||
|
autobins = false
|
||||||
|
autoexamples = false
|
||||||
|
autotests = false
|
||||||
|
autobenches = false
|
||||||
|
description = "Provides rendering functionality for Bevy Engine"
|
||||||
|
homepage = "https://bevy.org"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["bevy"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://github.com/bevyengine/bevy"
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = [
|
||||||
|
"-Zunstable-options",
|
||||||
|
"--generate-link-to-definition",
|
||||||
|
]
|
||||||
|
all-features = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
ci_limits = []
|
||||||
|
decoupled_naga = ["bevy_shader/decoupled_naga"]
|
||||||
|
detailed_trace = []
|
||||||
|
gles = ["wgpu/gles"]
|
||||||
|
morph = ["bevy_mesh/morph"]
|
||||||
|
multi_threaded = ["bevy_tasks/multi_threaded"]
|
||||||
|
raw_vulkan_init = ["wgpu/vulkan"]
|
||||||
|
serialize = ["bevy_mesh/serialize"]
|
||||||
|
shader_format_spirv = [
|
||||||
|
"bevy_shader/shader_format_spirv",
|
||||||
|
"wgpu/spirv",
|
||||||
|
]
|
||||||
|
spirv_shader_passthrough = ["wgpu/spirv"]
|
||||||
|
statically-linked-dxc = ["wgpu/static-dxc"]
|
||||||
|
trace = ["profiling"]
|
||||||
|
tracing-tracy = ["dep:tracy-client"]
|
||||||
|
vulkan-portability = ["wgpu/vulkan-portability"]
|
||||||
|
webgl = ["wgpu/webgl"]
|
||||||
|
webgpu = ["wgpu/webgpu"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "bevy_render"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies.async-channel]
|
||||||
|
version = "2.3.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_app]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_asset]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_camera]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_color]
|
||||||
|
version = "0.18.0"
|
||||||
|
features = [
|
||||||
|
"serialize",
|
||||||
|
"wgpu-types",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.bevy_derive]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_diagnostic]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_ecs]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_encase_derive]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_image]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_math]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_mesh]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_platform]
|
||||||
|
version = "0.18.0"
|
||||||
|
features = [
|
||||||
|
"std",
|
||||||
|
"serialize",
|
||||||
|
]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.bevy_reflect]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_render_macros]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_shader]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_tasks]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_time]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_transform]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_utils]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bevy_window]
|
||||||
|
version = "0.18.0"
|
||||||
|
|
||||||
|
[dependencies.bitflags]
|
||||||
|
version = "2"
|
||||||
|
|
||||||
|
[dependencies.bytemuck]
|
||||||
|
version = "1.5"
|
||||||
|
features = [
|
||||||
|
"derive",
|
||||||
|
"must_cast",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
version = "2"
|
||||||
|
features = ["from"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.downcast-rs]
|
||||||
|
version = "2"
|
||||||
|
features = ["std"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.encase]
|
||||||
|
version = "0.12"
|
||||||
|
|
||||||
|
[dependencies.fixedbitset]
|
||||||
|
version = "0.5"
|
||||||
|
|
||||||
|
[dependencies.glam]
|
||||||
|
version = "0.30.7"
|
||||||
|
features = [
|
||||||
|
"std",
|
||||||
|
"encase",
|
||||||
|
]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.image]
|
||||||
|
version = "0.25.2"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.indexmap]
|
||||||
|
version = "2"
|
||||||
|
|
||||||
|
[dependencies.naga]
|
||||||
|
version = "27"
|
||||||
|
features = ["wgsl-in"]
|
||||||
|
|
||||||
|
[dependencies.nonmax]
|
||||||
|
version = "0.5"
|
||||||
|
|
||||||
|
[dependencies.offset-allocator]
|
||||||
|
version = "0.2"
|
||||||
|
|
||||||
|
[dependencies.profiling]
|
||||||
|
version = "1"
|
||||||
|
features = ["profile-with-tracing"]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.smallvec]
|
||||||
|
version = "1"
|
||||||
|
features = ["const_new"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.thiserror]
|
||||||
|
version = "2"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.tracing]
|
||||||
|
version = "0.1"
|
||||||
|
features = ["std"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dependencies.tracy-client]
|
||||||
|
version = "0.18.3"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.variadics_please]
|
||||||
|
version = "1.1"
|
||||||
|
|
||||||
|
[dependencies.wgpu]
|
||||||
|
version = "27"
|
||||||
|
features = [
|
||||||
|
"wgsl",
|
||||||
|
"dx12",
|
||||||
|
"metal",
|
||||||
|
"vulkan",
|
||||||
|
"naga-ir",
|
||||||
|
"fragile-send-sync-non-atomic-wasm",
|
||||||
|
]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[dev-dependencies.proptest]
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies.send_wrapper]
|
||||||
|
version = "0.6.0"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies.bevy_app]
|
||||||
|
version = "0.18.0"
|
||||||
|
features = ["web"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies.bevy_platform]
|
||||||
|
version = "0.18.0"
|
||||||
|
features = ["web"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies.bevy_reflect]
|
||||||
|
version = "0.18.0"
|
||||||
|
features = ["web"]
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies.js-sys]
|
||||||
|
version = "0.3.83"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen]
|
||||||
|
version = "0.2"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
|
||||||
|
version = "0.3.67"
|
||||||
|
features = [
|
||||||
|
"Blob",
|
||||||
|
"Document",
|
||||||
|
"Element",
|
||||||
|
"HtmlElement",
|
||||||
|
"Node",
|
||||||
|
"Url",
|
||||||
|
"Window",
|
||||||
|
]
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
alloc_instead_of_core = "warn"
|
||||||
|
allow_attributes = "warn"
|
||||||
|
allow_attributes_without_reason = "warn"
|
||||||
|
doc_markdown = "warn"
|
||||||
|
manual_let_else = "warn"
|
||||||
|
match_same_arms = "warn"
|
||||||
|
needless_lifetimes = "allow"
|
||||||
|
nonstandard_macro_braces = "warn"
|
||||||
|
print_stderr = "warn"
|
||||||
|
print_stdout = "warn"
|
||||||
|
ptr_as_ptr = "warn"
|
||||||
|
ptr_cast_constness = "warn"
|
||||||
|
redundant_closure_for_method_calls = "warn"
|
||||||
|
redundant_else = "warn"
|
||||||
|
ref_as_ptr = "warn"
|
||||||
|
semicolon_if_nothing_returned = "warn"
|
||||||
|
std_instead_of_alloc = "warn"
|
||||||
|
std_instead_of_core = "warn"
|
||||||
|
too_long_first_doc_paragraph = "allow"
|
||||||
|
too_many_arguments = "allow"
|
||||||
|
type_complexity = "allow"
|
||||||
|
undocumented_unsafe_blocks = "warn"
|
||||||
|
unwrap_or_default = "warn"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
missing_docs = "warn"
|
||||||
|
unsafe_code = "deny"
|
||||||
|
unsafe_op_in_unsafe_fn = "warn"
|
||||||
|
unused_qualifications = "warn"
|
||||||
|
|
||||||
|
[lints.rust.unexpected_cfgs]
|
||||||
|
level = "warn"
|
||||||
|
priority = 0
|
||||||
|
check-cfg = ["cfg(docsrs_dep)"]
|
||||||
155
third_party/bevy_render/Cargo.toml.orig
vendored
Normal file
155
third_party/bevy_render/Cargo.toml.orig
vendored
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
[package]
|
||||||
|
name = "bevy_render"
|
||||||
|
version = "0.18.1"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Provides rendering functionality for Bevy Engine"
|
||||||
|
homepage = "https://bevy.org"
|
||||||
|
repository = "https://github.com/bevyengine/bevy"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
keywords = ["bevy"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# Bevy users should _never_ turn this feature on.
|
||||||
|
#
|
||||||
|
# Bevy/wgpu developers can turn this feature on to test a newer version of wgpu without needing to also update naga_oil.
|
||||||
|
#
|
||||||
|
# When turning this feature on, you can add the following to bevy/Cargo.toml (not this file), and then run `cargo update`:
|
||||||
|
# [patch.crates-io]
|
||||||
|
# wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "..." }
|
||||||
|
# wgpu-core = { git = "https://github.com/gfx-rs/wgpu", rev = "..." }
|
||||||
|
# wgpu-hal = { git = "https://github.com/gfx-rs/wgpu", rev = "..." }
|
||||||
|
# wgpu-types = { git = "https://github.com/gfx-rs/wgpu", rev = "..." }
|
||||||
|
decoupled_naga = ["bevy_shader/decoupled_naga"]
|
||||||
|
|
||||||
|
multi_threaded = ["bevy_tasks/multi_threaded"]
|
||||||
|
|
||||||
|
morph = ["bevy_mesh/morph"]
|
||||||
|
|
||||||
|
shader_format_spirv = ["bevy_shader/shader_format_spirv", "wgpu/spirv"]
|
||||||
|
|
||||||
|
# Enable SPIR-V shader passthrough
|
||||||
|
spirv_shader_passthrough = ["wgpu/spirv"]
|
||||||
|
|
||||||
|
# Statically linked DXC shader compiler for DirectX 12
|
||||||
|
# TODO: When wgpu switches to DirectX 12 instead of Vulkan by default on windows, make this a default feature
|
||||||
|
statically-linked-dxc = ["wgpu/static-dxc"]
|
||||||
|
|
||||||
|
# Forces the wgpu instance to be initialized using the raw Vulkan HAL, enabling additional configuration
|
||||||
|
raw_vulkan_init = ["wgpu/vulkan"]
|
||||||
|
|
||||||
|
trace = ["profiling"]
|
||||||
|
tracing-tracy = ["dep:tracy-client"]
|
||||||
|
ci_limits = []
|
||||||
|
webgl = ["wgpu/webgl"]
|
||||||
|
webgpu = ["wgpu/webgpu"]
|
||||||
|
vulkan-portability = ["wgpu/vulkan-portability"]
|
||||||
|
gles = ["wgpu/gles"]
|
||||||
|
detailed_trace = []
|
||||||
|
## Adds serialization support through `serde`.
|
||||||
|
serialize = ["bevy_mesh/serialize"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# bevy
|
||||||
|
bevy_app = { path = "../bevy_app", version = "0.18.0" }
|
||||||
|
bevy_asset = { path = "../bevy_asset", version = "0.18.0" }
|
||||||
|
bevy_color = { path = "../bevy_color", version = "0.18.0", features = [
|
||||||
|
"serialize",
|
||||||
|
"wgpu-types",
|
||||||
|
] }
|
||||||
|
bevy_derive = { path = "../bevy_derive", version = "0.18.0" }
|
||||||
|
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.18.0" }
|
||||||
|
bevy_ecs = { path = "../bevy_ecs", version = "0.18.0" }
|
||||||
|
bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.18.0" }
|
||||||
|
bevy_math = { path = "../bevy_math", version = "0.18.0" }
|
||||||
|
bevy_reflect = { path = "../bevy_reflect", version = "0.18.0" }
|
||||||
|
bevy_render_macros = { path = "macros", version = "0.18.0" }
|
||||||
|
bevy_time = { path = "../bevy_time", version = "0.18.0" }
|
||||||
|
bevy_transform = { path = "../bevy_transform", version = "0.18.0" }
|
||||||
|
bevy_window = { path = "../bevy_window", version = "0.18.0" }
|
||||||
|
bevy_utils = { path = "../bevy_utils", version = "0.18.0" }
|
||||||
|
bevy_tasks = { path = "../bevy_tasks", version = "0.18.0" }
|
||||||
|
bevy_image = { path = "../bevy_image", version = "0.18.0" }
|
||||||
|
bevy_mesh = { path = "../bevy_mesh", version = "0.18.0" }
|
||||||
|
bevy_camera = { path = "../bevy_camera", version = "0.18.0" }
|
||||||
|
bevy_shader = { path = "../bevy_shader", version = "0.18.0" }
|
||||||
|
bevy_platform = { path = "../bevy_platform", version = "0.18.0", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"serialize",
|
||||||
|
] }
|
||||||
|
|
||||||
|
# rendering
|
||||||
|
image = { version = "0.25.2", default-features = false }
|
||||||
|
|
||||||
|
# misc
|
||||||
|
# `fragile-send-sync-non-atomic-wasm` feature means we can't use Wasm threads for rendering
|
||||||
|
# It is enabled for now to avoid having to do a significant overhaul of the renderer just for wasm.
|
||||||
|
# When the 'atomics' feature is enabled `fragile-send-sync-non-atomic` does nothing
|
||||||
|
# and Bevy instead wraps `wgpu` types to verify they are not used off their origin thread.
|
||||||
|
wgpu = { version = "27", default-features = false, features = [
|
||||||
|
"wgsl",
|
||||||
|
"dx12",
|
||||||
|
"metal",
|
||||||
|
"vulkan",
|
||||||
|
"naga-ir",
|
||||||
|
"fragile-send-sync-non-atomic-wasm",
|
||||||
|
] }
|
||||||
|
naga = { version = "27", features = ["wgsl-in"] }
|
||||||
|
bytemuck = { version = "1.5", features = ["derive", "must_cast"] }
|
||||||
|
downcast-rs = { version = "2", default-features = false, features = ["std"] }
|
||||||
|
thiserror = { version = "2", default-features = false }
|
||||||
|
derive_more = { version = "2", default-features = false, features = ["from"] }
|
||||||
|
encase = "0.12"
|
||||||
|
glam = { version = "0.30.7", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"encase",
|
||||||
|
] }
|
||||||
|
# For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans.
|
||||||
|
profiling = { version = "1", features = [
|
||||||
|
"profile-with-tracing",
|
||||||
|
], optional = true }
|
||||||
|
async-channel = "2.3.0"
|
||||||
|
nonmax = "0.5"
|
||||||
|
smallvec = { version = "1", default-features = false, features = ["const_new"] }
|
||||||
|
offset-allocator = "0.2"
|
||||||
|
variadics_please = "1.1"
|
||||||
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
|
tracy-client = { version = "0.18.3", optional = true }
|
||||||
|
indexmap = { version = "2" }
|
||||||
|
fixedbitset = { version = "0.5" }
|
||||||
|
bitflags = "2"
|
||||||
|
|
||||||
|
[target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies]
|
||||||
|
send_wrapper = { version = "0.6.0" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
proptest = "1"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
js-sys = "0.3.83"
|
||||||
|
web-sys = { version = "0.3.67", features = [
|
||||||
|
'Blob',
|
||||||
|
'Document',
|
||||||
|
'Element',
|
||||||
|
'HtmlElement',
|
||||||
|
'Node',
|
||||||
|
'Url',
|
||||||
|
'Window',
|
||||||
|
] }
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.
|
||||||
|
bevy_app = { path = "../bevy_app", version = "0.18.0", default-features = false, features = [
|
||||||
|
"web",
|
||||||
|
] }
|
||||||
|
bevy_platform = { path = "../bevy_platform", version = "0.18.0", default-features = false, features = [
|
||||||
|
"web",
|
||||||
|
] }
|
||||||
|
bevy_reflect = { path = "../bevy_reflect", version = "0.18.0", default-features = false, features = [
|
||||||
|
"web",
|
||||||
|
] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
|
||||||
|
all-features = true
|
||||||
176
third_party/bevy_render/LICENSE-APACHE
vendored
Normal file
176
third_party/bevy_render/LICENSE-APACHE
vendored
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
19
third_party/bevy_render/LICENSE-MIT
vendored
Normal file
19
third_party/bevy_render/LICENSE-MIT
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
7
third_party/bevy_render/README.md
vendored
Normal file
7
third_party/bevy_render/README.md
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Bevy Render
|
||||||
|
|
||||||
|
[](https://github.com/bevyengine/bevy#license)
|
||||||
|
[](https://crates.io/crates/bevy_render)
|
||||||
|
[](https://crates.io/crates/bevy_render)
|
||||||
|
[](https://docs.rs/bevy_render/latest/bevy_render/)
|
||||||
|
[](https://discord.gg/bevy)
|
||||||
62
third_party/bevy_render/src/alpha.rs
vendored
Normal file
62
third_party/bevy_render/src/alpha.rs
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||||
|
|
||||||
|
// TODO: add discussion about performance.
|
||||||
|
/// Sets how a material's base color alpha channel is used for transparency.
|
||||||
|
#[derive(Debug, Default, Reflect, Copy, Clone, PartialEq)]
|
||||||
|
#[reflect(Default, Debug, Clone)]
|
||||||
|
pub enum AlphaMode {
|
||||||
|
/// Base color alpha values are overridden to be fully opaque (1.0).
|
||||||
|
#[default]
|
||||||
|
Opaque,
|
||||||
|
/// Reduce transparency to fully opaque or fully transparent
|
||||||
|
/// based on a threshold.
|
||||||
|
///
|
||||||
|
/// Compares the base color alpha value to the specified threshold.
|
||||||
|
/// If the value is below the threshold,
|
||||||
|
/// considers the color to be fully transparent (alpha is set to 0.0).
|
||||||
|
/// If it is equal to or above the threshold,
|
||||||
|
/// considers the color to be fully opaque (alpha is set to 1.0).
|
||||||
|
Mask(f32),
|
||||||
|
/// The base color alpha value defines the opacity of the color.
|
||||||
|
/// Standard alpha-blending is used to blend the fragment's color
|
||||||
|
/// with the color behind it.
|
||||||
|
Blend,
|
||||||
|
/// Similar to [`AlphaMode::Blend`], however assumes RGB channel values are
|
||||||
|
/// [premultiplied](https://en.wikipedia.org/wiki/Alpha_compositing#Straight_versus_premultiplied).
|
||||||
|
///
|
||||||
|
/// For otherwise constant RGB values, behaves more like [`AlphaMode::Blend`] for
|
||||||
|
/// alpha values closer to 1.0, and more like [`AlphaMode::Add`] for
|
||||||
|
/// alpha values closer to 0.0.
|
||||||
|
///
|
||||||
|
/// Can be used to avoid “border” or “outline” artifacts that can occur
|
||||||
|
/// when using plain alpha-blended textures.
|
||||||
|
Premultiplied,
|
||||||
|
/// Spreads the fragment out over a hardware-dependent number of sample
|
||||||
|
/// locations proportional to the alpha value. This requires multisample
|
||||||
|
/// antialiasing; if MSAA isn't on, this is identical to
|
||||||
|
/// [`AlphaMode::Mask`] with a value of 0.5.
|
||||||
|
///
|
||||||
|
/// Alpha to coverage provides improved performance and better visual
|
||||||
|
/// fidelity over [`AlphaMode::Blend`], as Bevy doesn't have to sort objects
|
||||||
|
/// when it's in use. It's especially useful for complex transparent objects
|
||||||
|
/// like foliage.
|
||||||
|
///
|
||||||
|
/// [alpha to coverage]: https://en.wikipedia.org/wiki/Alpha_to_coverage
|
||||||
|
AlphaToCoverage,
|
||||||
|
/// Combines the color of the fragments with the colors behind them in an
|
||||||
|
/// additive process, (i.e. like light) producing lighter results.
|
||||||
|
///
|
||||||
|
/// Black produces no effect. Alpha values can be used to modulate the result.
|
||||||
|
///
|
||||||
|
/// Useful for effects like holograms, ghosts, lasers and other energy beams.
|
||||||
|
Add,
|
||||||
|
/// Combines the color of the fragments with the colors behind them in a
|
||||||
|
/// multiplicative process, (i.e. like pigments) producing darker results.
|
||||||
|
///
|
||||||
|
/// White produces no effect. Alpha values can be used to modulate the result.
|
||||||
|
///
|
||||||
|
/// Useful for effects like stained glass, window tint film and some colored liquids.
|
||||||
|
Multiply,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for AlphaMode {}
|
||||||
2187
third_party/bevy_render/src/batching/gpu_preprocessing.rs
vendored
Normal file
2187
third_party/bevy_render/src/batching/gpu_preprocessing.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
225
third_party/bevy_render/src/batching/mod.rs
vendored
Normal file
225
third_party/bevy_render/src/batching/mod.rs
vendored
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
use bevy_ecs::{
|
||||||
|
component::Component,
|
||||||
|
entity::Entity,
|
||||||
|
system::{ResMut, SystemParam, SystemParamItem},
|
||||||
|
};
|
||||||
|
use bytemuck::Pod;
|
||||||
|
use gpu_preprocessing::UntypedPhaseIndirectParametersBuffers;
|
||||||
|
use nonmax::NonMaxU32;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
render_phase::{
|
||||||
|
BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItemExtraIndex,
|
||||||
|
SortedPhaseItem, SortedRenderPhase, ViewBinnedRenderPhases,
|
||||||
|
},
|
||||||
|
render_resource::{CachedRenderPipelineId, GpuArrayBufferable},
|
||||||
|
sync_world::MainEntity,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod gpu_preprocessing;
|
||||||
|
pub mod no_gpu_preprocessing;
|
||||||
|
|
||||||
|
/// Add this component to mesh entities to disable automatic batching
|
||||||
|
#[derive(Component, Default, Clone, Copy)]
|
||||||
|
pub struct NoAutomaticBatching;
|
||||||
|
|
||||||
|
/// Data necessary to be equal for two draw commands to be mergeable
|
||||||
|
///
|
||||||
|
/// This is based on the following assumptions:
|
||||||
|
/// - Only entities with prepared assets (pipelines, materials, meshes) are
|
||||||
|
/// queued to phases
|
||||||
|
/// - View bindings are constant across a phase for a given draw function as
|
||||||
|
/// phases are per-view
|
||||||
|
/// - `batch_and_prepare_render_phase` is the only system that performs this
|
||||||
|
/// batching and has sole responsibility for preparing the per-object data.
|
||||||
|
/// As such the mesh binding and dynamic offsets are assumed to only be
|
||||||
|
/// variable as a result of the `batch_and_prepare_render_phase` system, e.g.
|
||||||
|
/// due to having to split data across separate uniform bindings within the
|
||||||
|
/// same buffer due to the maximum uniform buffer binding size.
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct BatchMeta<T: PartialEq> {
|
||||||
|
/// The pipeline id encompasses all pipeline configuration including vertex
|
||||||
|
/// buffers and layouts, shaders and their specializations, bind group
|
||||||
|
/// layouts, etc.
|
||||||
|
pipeline_id: CachedRenderPipelineId,
|
||||||
|
/// The draw function id defines the `RenderCommands` that are called to
|
||||||
|
/// set the pipeline and bindings, and make the draw command
|
||||||
|
draw_function_id: DrawFunctionId,
|
||||||
|
dynamic_offset: Option<NonMaxU32>,
|
||||||
|
user_data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: PartialEq> BatchMeta<T> {
|
||||||
|
fn new(item: &impl CachedRenderPipelinePhaseItem, user_data: T) -> Self {
|
||||||
|
BatchMeta {
|
||||||
|
pipeline_id: item.cached_pipeline(),
|
||||||
|
draw_function_id: item.draw_function(),
|
||||||
|
dynamic_offset: match item.extra_index() {
|
||||||
|
PhaseItemExtraIndex::DynamicOffset(dynamic_offset) => {
|
||||||
|
NonMaxU32::new(dynamic_offset)
|
||||||
|
}
|
||||||
|
PhaseItemExtraIndex::None | PhaseItemExtraIndex::IndirectParametersIndex { .. } => {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user_data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait to support getting data used for batching draw commands via phase
|
||||||
|
/// items.
|
||||||
|
///
|
||||||
|
/// This is a simple version that only allows for sorting, not binning, as well
|
||||||
|
/// as only CPU processing, not GPU preprocessing. For these fancier features,
|
||||||
|
/// see [`GetFullBatchData`].
|
||||||
|
pub trait GetBatchData {
|
||||||
|
/// The system parameters [`GetBatchData::get_batch_data`] needs in
|
||||||
|
/// order to compute the batch data.
|
||||||
|
type Param: SystemParam + 'static;
|
||||||
|
/// Data used for comparison between phase items. If the pipeline id, draw
|
||||||
|
/// function id, per-instance data buffer dynamic offset and this data
|
||||||
|
/// matches, the draws can be batched.
|
||||||
|
type CompareData: PartialEq;
|
||||||
|
/// The per-instance data to be inserted into the
|
||||||
|
/// [`crate::render_resource::GpuArrayBuffer`] containing these data for all
|
||||||
|
/// instances.
|
||||||
|
type BufferData: GpuArrayBufferable + Sync + Send + 'static;
|
||||||
|
/// Get the per-instance data to be inserted into the
|
||||||
|
/// [`crate::render_resource::GpuArrayBuffer`]. If the instance can be
|
||||||
|
/// batched, also return the data used for comparison when deciding whether
|
||||||
|
/// draws can be batched, else return None for the `CompareData`.
|
||||||
|
///
|
||||||
|
/// This is only called when building instance data on CPU. In the GPU
|
||||||
|
/// instance data building path, we use
|
||||||
|
/// [`GetFullBatchData::get_index_and_compare_data`] instead.
|
||||||
|
fn get_batch_data(
|
||||||
|
param: &SystemParamItem<Self::Param>,
|
||||||
|
query_item: (Entity, MainEntity),
|
||||||
|
) -> Option<(Self::BufferData, Option<Self::CompareData>)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait to support getting data used for batching draw commands via phase
|
||||||
|
/// items.
|
||||||
|
///
|
||||||
|
/// This version allows for binning and GPU preprocessing.
|
||||||
|
pub trait GetFullBatchData: GetBatchData {
|
||||||
|
/// The per-instance data that was inserted into the
|
||||||
|
/// [`crate::render_resource::BufferVec`] during extraction.
|
||||||
|
type BufferInputData: Pod + Default + Sync + Send;
|
||||||
|
|
||||||
|
/// Get the per-instance data to be inserted into the
|
||||||
|
/// [`crate::render_resource::GpuArrayBuffer`].
|
||||||
|
///
|
||||||
|
/// This is only called when building uniforms on CPU. In the GPU instance
|
||||||
|
/// buffer building path, we use
|
||||||
|
/// [`GetFullBatchData::get_index_and_compare_data`] instead.
|
||||||
|
fn get_binned_batch_data(
|
||||||
|
param: &SystemParamItem<Self::Param>,
|
||||||
|
query_item: MainEntity,
|
||||||
|
) -> Option<Self::BufferData>;
|
||||||
|
|
||||||
|
/// Returns the index of the [`GetFullBatchData::BufferInputData`] that the
|
||||||
|
/// GPU preprocessing phase will use.
|
||||||
|
///
|
||||||
|
/// We already inserted the [`GetFullBatchData::BufferInputData`] during the
|
||||||
|
/// extraction phase before we got here, so this function shouldn't need to
|
||||||
|
/// look up any render data. If CPU instance buffer building is in use, this
|
||||||
|
/// function will never be called.
|
||||||
|
fn get_index_and_compare_data(
|
||||||
|
param: &SystemParamItem<Self::Param>,
|
||||||
|
query_item: MainEntity,
|
||||||
|
) -> Option<(NonMaxU32, Option<Self::CompareData>)>;
|
||||||
|
|
||||||
|
/// Returns the index of the [`GetFullBatchData::BufferInputData`] that the
|
||||||
|
/// GPU preprocessing phase will use.
|
||||||
|
///
|
||||||
|
/// We already inserted the [`GetFullBatchData::BufferInputData`] during the
|
||||||
|
/// extraction phase before we got here, so this function shouldn't need to
|
||||||
|
/// look up any render data.
|
||||||
|
///
|
||||||
|
/// This function is currently only called for unbatchable entities when GPU
|
||||||
|
/// instance buffer building is in use. For batchable entities, the uniform
|
||||||
|
/// index is written during queuing (e.g. in `queue_material_meshes`). In
|
||||||
|
/// the case of CPU instance buffer building, the CPU writes the uniforms,
|
||||||
|
/// so there's no index to return.
|
||||||
|
fn get_binned_index(
|
||||||
|
param: &SystemParamItem<Self::Param>,
|
||||||
|
query_item: MainEntity,
|
||||||
|
) -> Option<NonMaxU32>;
|
||||||
|
|
||||||
|
/// Writes the [`gpu_preprocessing::IndirectParametersGpuMetadata`]
|
||||||
|
/// necessary to draw this batch into the given metadata buffer at the given
|
||||||
|
/// index.
|
||||||
|
///
|
||||||
|
/// This is only used if GPU culling is enabled (which requires GPU
|
||||||
|
/// preprocessing).
|
||||||
|
///
|
||||||
|
/// * `indexed` is true if the mesh is indexed or false if it's non-indexed.
|
||||||
|
///
|
||||||
|
/// * `base_output_index` is the index of the first mesh instance in this
|
||||||
|
/// batch in the `MeshUniform` output buffer.
|
||||||
|
///
|
||||||
|
/// * `batch_set_index` is the index of the batch set in the
|
||||||
|
/// [`gpu_preprocessing::IndirectBatchSet`] buffer, if this batch belongs to
|
||||||
|
/// a batch set.
|
||||||
|
///
|
||||||
|
/// * `indirect_parameters_buffers` is the buffer in which to write the
|
||||||
|
/// metadata.
|
||||||
|
///
|
||||||
|
/// * `indirect_parameters_offset` is the index in that buffer at which to
|
||||||
|
/// write the metadata.
|
||||||
|
fn write_batch_indirect_parameters_metadata(
|
||||||
|
indexed: bool,
|
||||||
|
base_output_index: u32,
|
||||||
|
batch_set_index: Option<NonMaxU32>,
|
||||||
|
indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers,
|
||||||
|
indirect_parameters_offset: u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sorts a render phase that uses bins.
|
||||||
|
pub fn sort_binned_render_phase<BPI>(mut phases: ResMut<ViewBinnedRenderPhases<BPI>>)
|
||||||
|
where
|
||||||
|
BPI: BinnedPhaseItem,
|
||||||
|
{
|
||||||
|
for phase in phases.values_mut() {
|
||||||
|
phase.multidrawable_meshes.sort_unstable_keys();
|
||||||
|
phase.batchable_meshes.sort_unstable_keys();
|
||||||
|
phase.unbatchable_meshes.sort_unstable_keys();
|
||||||
|
phase.non_mesh_items.sort_unstable_keys();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Batches the items in a sorted render phase.
|
||||||
|
///
|
||||||
|
/// This means comparing metadata needed to draw each phase item and trying to
|
||||||
|
/// combine the draws into a batch.
|
||||||
|
///
|
||||||
|
/// This is common code factored out from
|
||||||
|
/// [`gpu_preprocessing::batch_and_prepare_sorted_render_phase`] and
|
||||||
|
/// [`no_gpu_preprocessing::batch_and_prepare_sorted_render_phase`].
|
||||||
|
fn batch_and_prepare_sorted_render_phase<I, GBD>(
|
||||||
|
phase: &mut SortedRenderPhase<I>,
|
||||||
|
mut process_item: impl FnMut(&mut I) -> Option<GBD::CompareData>,
|
||||||
|
) where
|
||||||
|
I: CachedRenderPipelinePhaseItem + SortedPhaseItem,
|
||||||
|
GBD: GetBatchData,
|
||||||
|
{
|
||||||
|
let items = phase.items.iter_mut().map(|item| {
|
||||||
|
let batch_data = match process_item(item) {
|
||||||
|
Some(compare_data) if I::AUTOMATIC_BATCHING => Some(BatchMeta::new(item, compare_data)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
(item.batch_range_mut(), batch_data)
|
||||||
|
});
|
||||||
|
|
||||||
|
items.reduce(|(start_range, prev_batch_meta), (range, batch_meta)| {
|
||||||
|
if batch_meta.is_some() && prev_batch_meta == batch_meta {
|
||||||
|
start_range.end = range.end;
|
||||||
|
(start_range, prev_batch_meta)
|
||||||
|
} else {
|
||||||
|
(range, batch_meta)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
182
third_party/bevy_render/src/batching/no_gpu_preprocessing.rs
vendored
Normal file
182
third_party/bevy_render/src/batching/no_gpu_preprocessing.rs
vendored
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
//! Batching functionality when GPU preprocessing isn't in use.
|
||||||
|
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::entity::Entity;
|
||||||
|
use bevy_ecs::resource::Resource;
|
||||||
|
use bevy_ecs::system::{Res, ResMut, StaticSystemParam};
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use tracing::error;
|
||||||
|
use wgpu::{BindingResource, Limits};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
render_phase::{
|
||||||
|
BinnedPhaseItem, BinnedRenderPhaseBatch, BinnedRenderPhaseBatchSets,
|
||||||
|
CachedRenderPipelinePhaseItem, PhaseItemExtraIndex, SortedPhaseItem,
|
||||||
|
ViewBinnedRenderPhases, ViewSortedRenderPhases,
|
||||||
|
},
|
||||||
|
render_resource::{GpuArrayBuffer, GpuArrayBufferable},
|
||||||
|
renderer::{RenderDevice, RenderQueue},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{GetBatchData, GetFullBatchData};
|
||||||
|
|
||||||
|
/// The GPU buffers holding the data needed to render batches.
|
||||||
|
///
|
||||||
|
/// For example, in the 3D PBR pipeline this holds `MeshUniform`s, which are the
|
||||||
|
/// `BD` type parameter in that mode.
|
||||||
|
#[derive(Resource, Deref, DerefMut)]
|
||||||
|
pub struct BatchedInstanceBuffer<BD>(pub GpuArrayBuffer<BD>)
|
||||||
|
where
|
||||||
|
BD: GpuArrayBufferable + Sync + Send + 'static;
|
||||||
|
|
||||||
|
impl<BD> BatchedInstanceBuffer<BD>
|
||||||
|
where
|
||||||
|
BD: GpuArrayBufferable + Sync + Send + 'static,
|
||||||
|
{
|
||||||
|
/// Creates a new buffer.
|
||||||
|
pub fn new(limits: &Limits) -> Self {
|
||||||
|
BatchedInstanceBuffer(GpuArrayBuffer::new(limits))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the binding of the buffer that contains the per-instance data.
|
||||||
|
///
|
||||||
|
/// If we're in the GPU instance buffer building mode, this buffer needs to
|
||||||
|
/// be filled in via a compute shader.
|
||||||
|
pub fn instance_data_binding(&self) -> Option<BindingResource<'_>> {
|
||||||
|
self.binding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A system that clears out the [`BatchedInstanceBuffer`] for the frame.
|
||||||
|
///
|
||||||
|
/// This needs to run before the CPU batched instance buffers are used.
|
||||||
|
pub fn clear_batched_cpu_instance_buffers<GBD>(
|
||||||
|
cpu_batched_instance_buffer: Option<ResMut<BatchedInstanceBuffer<GBD::BufferData>>>,
|
||||||
|
) where
|
||||||
|
GBD: GetBatchData,
|
||||||
|
{
|
||||||
|
if let Some(mut cpu_batched_instance_buffer) = cpu_batched_instance_buffer {
|
||||||
|
cpu_batched_instance_buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Batch the items in a sorted render phase, when GPU instance buffer building
|
||||||
|
/// isn't in use. This means comparing metadata needed to draw each phase item
|
||||||
|
/// and trying to combine the draws into a batch.
|
||||||
|
pub fn batch_and_prepare_sorted_render_phase<I, GBD>(
|
||||||
|
batched_instance_buffer: ResMut<BatchedInstanceBuffer<GBD::BufferData>>,
|
||||||
|
mut phases: ResMut<ViewSortedRenderPhases<I>>,
|
||||||
|
param: StaticSystemParam<GBD::Param>,
|
||||||
|
) where
|
||||||
|
I: CachedRenderPipelinePhaseItem + SortedPhaseItem,
|
||||||
|
GBD: GetBatchData,
|
||||||
|
{
|
||||||
|
let system_param_item = param.into_inner();
|
||||||
|
|
||||||
|
// We only process CPU-built batch data in this function.
|
||||||
|
let batched_instance_buffer = batched_instance_buffer.into_inner();
|
||||||
|
|
||||||
|
for phase in phases.values_mut() {
|
||||||
|
super::batch_and_prepare_sorted_render_phase::<I, GBD>(phase, |item| {
|
||||||
|
let (buffer_data, compare_data) =
|
||||||
|
GBD::get_batch_data(&system_param_item, (item.entity(), item.main_entity()))?;
|
||||||
|
let buffer_index = batched_instance_buffer.push(buffer_data);
|
||||||
|
|
||||||
|
let index = buffer_index.index;
|
||||||
|
let (batch_range, extra_index) = item.batch_range_and_extra_index_mut();
|
||||||
|
*batch_range = index..index + 1;
|
||||||
|
*extra_index = PhaseItemExtraIndex::maybe_dynamic_offset(buffer_index.dynamic_offset);
|
||||||
|
|
||||||
|
compare_data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates batches for a render phase that uses bins, when GPU batch data
|
||||||
|
/// building isn't in use.
|
||||||
|
pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
|
||||||
|
gpu_array_buffer: ResMut<BatchedInstanceBuffer<GFBD::BufferData>>,
|
||||||
|
mut phases: ResMut<ViewBinnedRenderPhases<BPI>>,
|
||||||
|
param: StaticSystemParam<GFBD::Param>,
|
||||||
|
) where
|
||||||
|
BPI: BinnedPhaseItem,
|
||||||
|
GFBD: GetFullBatchData,
|
||||||
|
{
|
||||||
|
let gpu_array_buffer = gpu_array_buffer.into_inner();
|
||||||
|
let system_param_item = param.into_inner();
|
||||||
|
|
||||||
|
for phase in phases.values_mut() {
|
||||||
|
// Prepare batchables.
|
||||||
|
|
||||||
|
for bin in phase.batchable_meshes.values_mut() {
|
||||||
|
let mut batch_set: SmallVec<[BinnedRenderPhaseBatch; 1]> = smallvec![];
|
||||||
|
for main_entity in bin.entities().keys() {
|
||||||
|
let Some(buffer_data) =
|
||||||
|
GFBD::get_binned_batch_data(&system_param_item, *main_entity)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let instance = gpu_array_buffer.push(buffer_data);
|
||||||
|
|
||||||
|
// If the dynamic offset has changed, flush the batch.
|
||||||
|
//
|
||||||
|
// This is the only time we ever have more than one batch per
|
||||||
|
// bin. Note that dynamic offsets are only used on platforms
|
||||||
|
// with no storage buffers.
|
||||||
|
if !batch_set.last().is_some_and(|batch| {
|
||||||
|
batch.instance_range.end == instance.index
|
||||||
|
&& batch.extra_index
|
||||||
|
== PhaseItemExtraIndex::maybe_dynamic_offset(instance.dynamic_offset)
|
||||||
|
}) {
|
||||||
|
batch_set.push(BinnedRenderPhaseBatch {
|
||||||
|
representative_entity: (Entity::PLACEHOLDER, *main_entity),
|
||||||
|
instance_range: instance.index..instance.index,
|
||||||
|
extra_index: PhaseItemExtraIndex::maybe_dynamic_offset(
|
||||||
|
instance.dynamic_offset,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(batch) = batch_set.last_mut() {
|
||||||
|
batch.instance_range.end = instance.index + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match phase.batch_sets {
|
||||||
|
BinnedRenderPhaseBatchSets::DynamicUniforms(ref mut batch_sets) => {
|
||||||
|
batch_sets.push(batch_set);
|
||||||
|
}
|
||||||
|
BinnedRenderPhaseBatchSets::Direct(_)
|
||||||
|
| BinnedRenderPhaseBatchSets::MultidrawIndirect { .. } => {
|
||||||
|
error!(
|
||||||
|
"Dynamic uniform batch sets should be used when GPU preprocessing is off"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare unbatchables.
|
||||||
|
for unbatchables in phase.unbatchable_meshes.values_mut() {
|
||||||
|
for main_entity in unbatchables.entities.keys() {
|
||||||
|
let Some(buffer_data) =
|
||||||
|
GFBD::get_binned_batch_data(&system_param_item, *main_entity)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let instance = gpu_array_buffer.push(buffer_data);
|
||||||
|
unbatchables.buffer_indices.add(instance.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the instance buffer data to the GPU.
|
||||||
|
pub fn write_batched_instance_buffer<GBD>(
|
||||||
|
render_device: Res<RenderDevice>,
|
||||||
|
render_queue: Res<RenderQueue>,
|
||||||
|
mut cpu_batched_instance_buffer: ResMut<BatchedInstanceBuffer<GBD::BufferData>>,
|
||||||
|
) where
|
||||||
|
GBD: GetBatchData,
|
||||||
|
{
|
||||||
|
cpu_batched_instance_buffer.write_buffer(&render_device, &render_queue);
|
||||||
|
}
|
||||||
37
third_party/bevy_render/src/bindless.wgsl
vendored
Normal file
37
third_party/bevy_render/src/bindless.wgsl
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Defines the common arrays used to access bindless resources.
|
||||||
|
//
|
||||||
|
// This need to be kept up to date with the `BINDING_NUMBERS` table in
|
||||||
|
// `bindless.rs`.
|
||||||
|
//
|
||||||
|
// You access these by indexing into the bindless index table, and from there
|
||||||
|
// indexing into the appropriate binding array. For example, to access the base
|
||||||
|
// color texture of a `StandardMaterial` in bindless mode, write
|
||||||
|
// `bindless_textures_2d[materials[slot].base_color_texture]`, where
|
||||||
|
// `materials` is the bindless index table and `slot` is the index into that
|
||||||
|
// table (which can be found in the `Mesh`).
|
||||||
|
|
||||||
|
#define_import_path bevy_render::bindless
|
||||||
|
|
||||||
|
#ifdef BINDLESS
|
||||||
|
|
||||||
|
// Binding 0 is the bindless index table.
|
||||||
|
// Filtering samplers.
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var bindless_samplers_filtering: binding_array<sampler>;
|
||||||
|
// Non-filtering samplers (nearest neighbor).
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var bindless_samplers_non_filtering: binding_array<sampler>;
|
||||||
|
// Comparison samplers (typically for shadow mapping).
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(3) var bindless_samplers_comparison: binding_array<sampler>;
|
||||||
|
// 1D textures.
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(4) var bindless_textures_1d: binding_array<texture_1d<f32>>;
|
||||||
|
// 2D textures.
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(5) var bindless_textures_2d: binding_array<texture_2d<f32>>;
|
||||||
|
// 2D array textures.
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(6) var bindless_textures_2d_array: binding_array<texture_2d_array<f32>>;
|
||||||
|
// 3D textures.
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(7) var bindless_textures_3d: binding_array<texture_3d<f32>>;
|
||||||
|
// Cubemap textures.
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(8) var bindless_textures_cube: binding_array<texture_cube<f32>>;
|
||||||
|
// Cubemap array textures.
|
||||||
|
@group(#{MATERIAL_BIND_GROUP}) @binding(9) var bindless_textures_cube_array: binding_array<texture_cube_array<f32>>;
|
||||||
|
|
||||||
|
#endif // BINDLESS
|
||||||
702
third_party/bevy_render/src/camera.rs
vendored
Normal file
702
third_party/bevy_render/src/camera.rs
vendored
Normal file
@ -0,0 +1,702 @@
|
|||||||
|
use crate::{
|
||||||
|
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
||||||
|
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||||
|
extract_resource::{ExtractResource, ExtractResourcePlugin},
|
||||||
|
render_asset::RenderAssets,
|
||||||
|
render_graph::{CameraDriverNode, InternedRenderSubGraph, RenderGraph, RenderSubGraph},
|
||||||
|
render_resource::TextureView,
|
||||||
|
sync_world::{RenderEntity, SyncToRenderWorld},
|
||||||
|
texture::{GpuImage, ManualTextureViews},
|
||||||
|
view::{
|
||||||
|
ColorGrading, ExtractedView, ExtractedWindows, Hdr, Msaa, NoIndirectDrawing,
|
||||||
|
RenderVisibleEntities, RetainedViewEntity, ViewUniformOffset,
|
||||||
|
},
|
||||||
|
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bevy_app::{App, Plugin, PostStartup, PostUpdate};
|
||||||
|
use bevy_asset::{AssetEvent, AssetEventSystems, AssetId, Assets};
|
||||||
|
use bevy_camera::{
|
||||||
|
primitives::Frustum,
|
||||||
|
visibility::{self, RenderLayers, VisibleEntities},
|
||||||
|
Camera, Camera2d, Camera3d, CameraMainTextureUsages, CameraOutputMode, CameraUpdateSystems,
|
||||||
|
ClearColor, ClearColorConfig, Exposure, ManualTextureViewHandle, MsaaWriteback,
|
||||||
|
NormalizedRenderTarget, Projection, RenderTarget, RenderTargetInfo, Viewport,
|
||||||
|
};
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::{
|
||||||
|
change_detection::DetectChanges,
|
||||||
|
component::Component,
|
||||||
|
entity::{ContainsEntity, Entity},
|
||||||
|
error::BevyError,
|
||||||
|
lifecycle::HookContext,
|
||||||
|
message::MessageReader,
|
||||||
|
prelude::With,
|
||||||
|
query::{Has, QueryItem},
|
||||||
|
reflect::ReflectComponent,
|
||||||
|
resource::Resource,
|
||||||
|
schedule::IntoScheduleConfigs,
|
||||||
|
system::{Commands, Query, Res, ResMut},
|
||||||
|
world::DeferredWorld,
|
||||||
|
};
|
||||||
|
use bevy_image::Image;
|
||||||
|
use bevy_math::{uvec2, vec2, Mat4, URect, UVec2, UVec4, Vec2};
|
||||||
|
use bevy_platform::collections::{HashMap, HashSet};
|
||||||
|
use bevy_reflect::prelude::*;
|
||||||
|
use bevy_transform::components::GlobalTransform;
|
||||||
|
use bevy_window::{PrimaryWindow, Window, WindowCreated, WindowResized, WindowScaleFactorChanged};
|
||||||
|
use tracing::warn;
|
||||||
|
use wgpu::TextureFormat;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CameraPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CameraPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.register_required_components::<Camera, Msaa>()
|
||||||
|
.register_required_components::<Camera, SyncToRenderWorld>()
|
||||||
|
.register_required_components::<Camera3d, ColorGrading>()
|
||||||
|
.register_required_components::<Camera3d, Exposure>()
|
||||||
|
.add_plugins((
|
||||||
|
ExtractResourcePlugin::<ClearColor>::default(),
|
||||||
|
ExtractComponentPlugin::<CameraMainTextureUsages>::default(),
|
||||||
|
))
|
||||||
|
.add_systems(PostStartup, camera_system.in_set(CameraUpdateSystems))
|
||||||
|
.add_systems(
|
||||||
|
PostUpdate,
|
||||||
|
camera_system
|
||||||
|
.in_set(CameraUpdateSystems)
|
||||||
|
.before(AssetEventSystems)
|
||||||
|
.before(visibility::update_frusta),
|
||||||
|
);
|
||||||
|
app.world_mut()
|
||||||
|
.register_component_hooks::<Camera>()
|
||||||
|
.on_add(warn_on_no_render_graph);
|
||||||
|
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app
|
||||||
|
.init_resource::<SortedCameras>()
|
||||||
|
.add_systems(ExtractSchedule, extract_cameras)
|
||||||
|
.add_systems(Render, sort_cameras.in_set(RenderSystems::ManageViews));
|
||||||
|
let camera_driver_node = CameraDriverNode::new(render_app.world_mut());
|
||||||
|
let mut render_graph = render_app.world_mut().resource_mut::<RenderGraph>();
|
||||||
|
render_graph.add_node(crate::graph::CameraDriverLabel, camera_driver_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) {
|
||||||
|
if !world.entity(entity).contains::<CameraRenderGraph>() {
|
||||||
|
warn!("{}Entity {entity} has a `Camera` component, but it doesn't have a render graph configured. Usually, adding a `Camera2d` or `Camera3d` component will work.
|
||||||
|
However, you may instead need to enable `bevy_core_pipeline`, or may want to manually add a `CameraRenderGraph` component to create a custom render graph.", caller.map(|location|format!("{location}: ")).unwrap_or_default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtractResource for ClearColor {
|
||||||
|
type Source = Self;
|
||||||
|
|
||||||
|
fn extract_resource(source: &Self::Source) -> Self {
|
||||||
|
source.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ExtractComponent for CameraMainTextureUsages {
|
||||||
|
type QueryData = &'static Self;
|
||||||
|
type QueryFilter = ();
|
||||||
|
type Out = Self;
|
||||||
|
|
||||||
|
fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
|
||||||
|
Some(*item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ExtractComponent for Camera2d {
|
||||||
|
type QueryData = &'static Self;
|
||||||
|
type QueryFilter = With<Camera>;
|
||||||
|
type Out = Self;
|
||||||
|
|
||||||
|
fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
|
||||||
|
Some(item.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ExtractComponent for Camera3d {
|
||||||
|
type QueryData = &'static Self;
|
||||||
|
type QueryFilter = With<Camera>;
|
||||||
|
type Out = Self;
|
||||||
|
|
||||||
|
fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
|
||||||
|
Some(item.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configures the [`RenderGraph`] name assigned to be run for a given [`Camera`] entity.
|
||||||
|
#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)]
|
||||||
|
#[reflect(opaque)]
|
||||||
|
#[reflect(Component, Debug, Clone)]
|
||||||
|
pub struct CameraRenderGraph(InternedRenderSubGraph);
|
||||||
|
|
||||||
|
impl CameraRenderGraph {
|
||||||
|
/// Creates a new [`CameraRenderGraph`] from any string-like type.
|
||||||
|
#[inline]
|
||||||
|
pub fn new<T: RenderSubGraph>(name: T) -> Self {
|
||||||
|
Self(name.intern())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the graph name.
|
||||||
|
#[inline]
|
||||||
|
pub fn set<T: RenderSubGraph>(&mut self, name: T) {
|
||||||
|
self.0 = name.intern();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NormalizedRenderTargetExt {
|
||||||
|
fn get_texture_view<'a>(
|
||||||
|
&self,
|
||||||
|
windows: &'a ExtractedWindows,
|
||||||
|
images: &'a RenderAssets<GpuImage>,
|
||||||
|
manual_texture_views: &'a ManualTextureViews,
|
||||||
|
) -> Option<&'a TextureView>;
|
||||||
|
|
||||||
|
/// Retrieves the [`TextureFormat`] of this render target, if it exists.
|
||||||
|
fn get_texture_view_format<'a>(
|
||||||
|
&self,
|
||||||
|
windows: &'a ExtractedWindows,
|
||||||
|
images: &'a RenderAssets<GpuImage>,
|
||||||
|
manual_texture_views: &'a ManualTextureViews,
|
||||||
|
) -> Option<TextureFormat>;
|
||||||
|
|
||||||
|
fn get_render_target_info<'a>(
|
||||||
|
&self,
|
||||||
|
resolutions: impl IntoIterator<Item = (Entity, &'a Window)>,
|
||||||
|
images: &Assets<Image>,
|
||||||
|
manual_texture_views: &ManualTextureViews,
|
||||||
|
) -> Result<RenderTargetInfo, MissingRenderTargetInfoError>;
|
||||||
|
|
||||||
|
// Check if this render target is contained in the given changed windows or images.
|
||||||
|
fn is_changed(
|
||||||
|
&self,
|
||||||
|
changed_window_ids: &HashSet<Entity>,
|
||||||
|
changed_image_handles: &HashSet<&AssetId<Image>>,
|
||||||
|
) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NormalizedRenderTargetExt for NormalizedRenderTarget {
|
||||||
|
fn get_texture_view<'a>(
|
||||||
|
&self,
|
||||||
|
windows: &'a ExtractedWindows,
|
||||||
|
images: &'a RenderAssets<GpuImage>,
|
||||||
|
manual_texture_views: &'a ManualTextureViews,
|
||||||
|
) -> Option<&'a TextureView> {
|
||||||
|
match self {
|
||||||
|
NormalizedRenderTarget::Window(window_ref) => windows
|
||||||
|
.get(&window_ref.entity())
|
||||||
|
.and_then(|window| window.swap_chain_texture_view.as_ref()),
|
||||||
|
NormalizedRenderTarget::Image(image_target) => images
|
||||||
|
.get(&image_target.handle)
|
||||||
|
.map(|image| &image.texture_view),
|
||||||
|
NormalizedRenderTarget::TextureView(id) => {
|
||||||
|
manual_texture_views.get(id).map(|tex| &tex.texture_view)
|
||||||
|
}
|
||||||
|
NormalizedRenderTarget::None { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the texture view's [`TextureFormat`] of this render target, if it exists.
|
||||||
|
fn get_texture_view_format<'a>(
|
||||||
|
&self,
|
||||||
|
windows: &'a ExtractedWindows,
|
||||||
|
images: &'a RenderAssets<GpuImage>,
|
||||||
|
manual_texture_views: &'a ManualTextureViews,
|
||||||
|
) -> Option<TextureFormat> {
|
||||||
|
match self {
|
||||||
|
NormalizedRenderTarget::Window(window_ref) => windows
|
||||||
|
.get(&window_ref.entity())
|
||||||
|
.and_then(|window| window.swap_chain_texture_view_format),
|
||||||
|
NormalizedRenderTarget::Image(image_target) => images
|
||||||
|
.get(&image_target.handle)
|
||||||
|
.map(|image| image.texture_view_format.unwrap_or(image.texture_format)),
|
||||||
|
NormalizedRenderTarget::TextureView(id) => {
|
||||||
|
manual_texture_views.get(id).map(|tex| tex.view_format)
|
||||||
|
}
|
||||||
|
NormalizedRenderTarget::None { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_render_target_info<'a>(
|
||||||
|
&self,
|
||||||
|
resolutions: impl IntoIterator<Item = (Entity, &'a Window)>,
|
||||||
|
images: &Assets<Image>,
|
||||||
|
manual_texture_views: &ManualTextureViews,
|
||||||
|
) -> Result<RenderTargetInfo, MissingRenderTargetInfoError> {
|
||||||
|
match self {
|
||||||
|
NormalizedRenderTarget::Window(window_ref) => resolutions
|
||||||
|
.into_iter()
|
||||||
|
.find(|(entity, _)| *entity == window_ref.entity())
|
||||||
|
.map(|(_, window)| RenderTargetInfo {
|
||||||
|
physical_size: window.physical_size(),
|
||||||
|
scale_factor: window.resolution.scale_factor(),
|
||||||
|
})
|
||||||
|
.ok_or(MissingRenderTargetInfoError::Window {
|
||||||
|
window: window_ref.entity(),
|
||||||
|
}),
|
||||||
|
NormalizedRenderTarget::Image(image_target) => images
|
||||||
|
.get(&image_target.handle)
|
||||||
|
.map(|image| RenderTargetInfo {
|
||||||
|
physical_size: image.size(),
|
||||||
|
scale_factor: image_target.scale_factor,
|
||||||
|
})
|
||||||
|
.ok_or(MissingRenderTargetInfoError::Image {
|
||||||
|
image: image_target.handle.id(),
|
||||||
|
}),
|
||||||
|
NormalizedRenderTarget::TextureView(id) => manual_texture_views
|
||||||
|
.get(id)
|
||||||
|
.map(|tex| RenderTargetInfo {
|
||||||
|
physical_size: tex.size,
|
||||||
|
scale_factor: 1.0,
|
||||||
|
})
|
||||||
|
.ok_or(MissingRenderTargetInfoError::TextureView { texture_view: *id }),
|
||||||
|
NormalizedRenderTarget::None { width, height } => Ok(RenderTargetInfo {
|
||||||
|
physical_size: uvec2(*width, *height),
|
||||||
|
scale_factor: 1.0,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this render target is contained in the given changed windows or images.
|
||||||
|
fn is_changed(
|
||||||
|
&self,
|
||||||
|
changed_window_ids: &HashSet<Entity>,
|
||||||
|
changed_image_handles: &HashSet<&AssetId<Image>>,
|
||||||
|
) -> bool {
|
||||||
|
match self {
|
||||||
|
NormalizedRenderTarget::Window(window_ref) => {
|
||||||
|
changed_window_ids.contains(&window_ref.entity())
|
||||||
|
}
|
||||||
|
NormalizedRenderTarget::Image(image_target) => {
|
||||||
|
changed_image_handles.contains(&image_target.handle.id())
|
||||||
|
}
|
||||||
|
NormalizedRenderTarget::TextureView(_) => true,
|
||||||
|
NormalizedRenderTarget::None { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum MissingRenderTargetInfoError {
|
||||||
|
#[error("RenderTarget::Window missing ({window:?}): Make sure the provided entity has a Window component.")]
|
||||||
|
Window { window: Entity },
|
||||||
|
#[error("RenderTarget::Image missing ({image:?}): Make sure the Image's usages include RenderAssetUsages::MAIN_WORLD.")]
|
||||||
|
Image { image: AssetId<Image> },
|
||||||
|
#[error("RenderTarget::TextureView missing ({texture_view:?}): make sure the texture view handle was not removed.")]
|
||||||
|
TextureView {
|
||||||
|
texture_view: ManualTextureViewHandle,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// System in charge of updating a [`Camera`] when its window or projection changes.
|
||||||
|
///
|
||||||
|
/// The system detects window creation, resize, and scale factor change events to update the camera
|
||||||
|
/// [`Projection`] if needed.
|
||||||
|
///
|
||||||
|
/// ## World Resources
|
||||||
|
///
|
||||||
|
/// [`Res<Assets<Image>>`](Assets<Image>) -- For cameras that render to an image, this resource is used to
|
||||||
|
/// inspect information about the render target. This system will not access any other image assets.
|
||||||
|
///
|
||||||
|
/// [`OrthographicProjection`]: bevy_camera::OrthographicProjection
|
||||||
|
/// [`PerspectiveProjection`]: bevy_camera::PerspectiveProjection
|
||||||
|
pub fn camera_system(
|
||||||
|
mut window_resized_reader: MessageReader<WindowResized>,
|
||||||
|
mut window_created_reader: MessageReader<WindowCreated>,
|
||||||
|
mut window_scale_factor_changed_reader: MessageReader<WindowScaleFactorChanged>,
|
||||||
|
mut image_asset_event_reader: MessageReader<AssetEvent<Image>>,
|
||||||
|
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||||
|
windows: Query<(Entity, &Window)>,
|
||||||
|
images: Res<Assets<Image>>,
|
||||||
|
manual_texture_views: Res<ManualTextureViews>,
|
||||||
|
mut cameras: Query<(&mut Camera, &RenderTarget, &mut Projection)>,
|
||||||
|
) -> Result<(), BevyError> {
|
||||||
|
let primary_window = primary_window.iter().next();
|
||||||
|
|
||||||
|
let mut changed_window_ids = <HashSet<_>>::default();
|
||||||
|
changed_window_ids.extend(window_created_reader.read().map(|event| event.window));
|
||||||
|
changed_window_ids.extend(window_resized_reader.read().map(|event| event.window));
|
||||||
|
let scale_factor_changed_window_ids: HashSet<_> = window_scale_factor_changed_reader
|
||||||
|
.read()
|
||||||
|
.map(|event| event.window)
|
||||||
|
.collect();
|
||||||
|
changed_window_ids.extend(scale_factor_changed_window_ids.clone());
|
||||||
|
|
||||||
|
let changed_image_handles: HashSet<&AssetId<Image>> = image_asset_event_reader
|
||||||
|
.read()
|
||||||
|
.filter_map(|event| match event {
|
||||||
|
AssetEvent::Modified { id } | AssetEvent::Added { id } => Some(id),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (mut camera, render_target, mut camera_projection) in &mut cameras {
|
||||||
|
let mut viewport_size = camera
|
||||||
|
.viewport
|
||||||
|
.as_ref()
|
||||||
|
.map(|viewport| viewport.physical_size);
|
||||||
|
|
||||||
|
if let Some(normalized_target) = render_target.normalize(primary_window)
|
||||||
|
&& (normalized_target.is_changed(&changed_window_ids, &changed_image_handles)
|
||||||
|
|| camera.is_added()
|
||||||
|
|| camera_projection.is_changed()
|
||||||
|
|| camera.computed.old_viewport_size != viewport_size
|
||||||
|
|| camera.computed.old_sub_camera_view != camera.sub_camera_view)
|
||||||
|
{
|
||||||
|
let new_computed_target_info = normalized_target.get_render_target_info(
|
||||||
|
windows,
|
||||||
|
&images,
|
||||||
|
&manual_texture_views,
|
||||||
|
)?;
|
||||||
|
// Check for the scale factor changing, and resize the viewport if needed.
|
||||||
|
// This can happen when the window is moved between monitors with different DPIs.
|
||||||
|
// Without this, the viewport will take a smaller portion of the window moved to
|
||||||
|
// a higher DPI monitor.
|
||||||
|
if normalized_target.is_changed(&scale_factor_changed_window_ids, &HashSet::default())
|
||||||
|
&& let Some(old_scale_factor) = camera
|
||||||
|
.computed
|
||||||
|
.target_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|info| info.scale_factor)
|
||||||
|
{
|
||||||
|
let resize_factor = new_computed_target_info.scale_factor / old_scale_factor;
|
||||||
|
if let Some(ref mut viewport) = camera.viewport {
|
||||||
|
let resize = |vec: UVec2| (vec.as_vec2() * resize_factor).as_uvec2();
|
||||||
|
viewport.physical_position = resize(viewport.physical_position);
|
||||||
|
viewport.physical_size = resize(viewport.physical_size);
|
||||||
|
viewport_size = Some(viewport.physical_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This check is needed because when changing WindowMode to Fullscreen, the viewport may have invalid
|
||||||
|
// arguments due to a sudden change on the window size to a lower value.
|
||||||
|
// If the size of the window is lower, the viewport will match that lower value.
|
||||||
|
if let Some(viewport) = &mut camera.viewport {
|
||||||
|
viewport.clamp_to_size(new_computed_target_info.physical_size);
|
||||||
|
}
|
||||||
|
camera.computed.target_info = Some(new_computed_target_info);
|
||||||
|
if let Some(size) = camera.logical_viewport_size()
|
||||||
|
&& size.x != 0.0
|
||||||
|
&& size.y != 0.0
|
||||||
|
{
|
||||||
|
camera_projection.update(size.x, size.y);
|
||||||
|
camera.computed.clip_from_view = match &camera.sub_camera_view {
|
||||||
|
Some(sub_view) => camera_projection.get_clip_from_view_for_sub(sub_view),
|
||||||
|
None => camera_projection.get_clip_from_view(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if camera.computed.old_viewport_size != viewport_size {
|
||||||
|
camera.computed.old_viewport_size = viewport_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if camera.computed.old_sub_camera_view != camera.sub_camera_view {
|
||||||
|
camera.computed.old_sub_camera_view = camera.sub_camera_view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct ExtractedCamera {
|
||||||
|
pub target: Option<NormalizedRenderTarget>,
|
||||||
|
pub physical_viewport_size: Option<UVec2>,
|
||||||
|
pub physical_target_size: Option<UVec2>,
|
||||||
|
pub viewport: Option<Viewport>,
|
||||||
|
pub render_graph: InternedRenderSubGraph,
|
||||||
|
pub order: isize,
|
||||||
|
pub output_mode: CameraOutputMode,
|
||||||
|
pub msaa_writeback: MsaaWriteback,
|
||||||
|
pub clear_color: ClearColorConfig,
|
||||||
|
pub sorted_camera_index_for_target: usize,
|
||||||
|
pub exposure: f32,
|
||||||
|
pub hdr: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_cameras(
|
||||||
|
mut commands: Commands,
|
||||||
|
query: Extract<
|
||||||
|
Query<(
|
||||||
|
Entity,
|
||||||
|
RenderEntity,
|
||||||
|
&Camera,
|
||||||
|
&RenderTarget,
|
||||||
|
&CameraRenderGraph,
|
||||||
|
&GlobalTransform,
|
||||||
|
&VisibleEntities,
|
||||||
|
&Frustum,
|
||||||
|
(
|
||||||
|
Has<Hdr>,
|
||||||
|
Option<&ColorGrading>,
|
||||||
|
Option<&Exposure>,
|
||||||
|
Option<&TemporalJitter>,
|
||||||
|
Option<&MipBias>,
|
||||||
|
Option<&RenderLayers>,
|
||||||
|
Option<&Projection>,
|
||||||
|
Has<NoIndirectDrawing>,
|
||||||
|
),
|
||||||
|
)>,
|
||||||
|
>,
|
||||||
|
primary_window: Extract<Query<Entity, With<PrimaryWindow>>>,
|
||||||
|
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
|
||||||
|
mapper: Extract<Query<&RenderEntity>>,
|
||||||
|
) {
|
||||||
|
let primary_window = primary_window.iter().next();
|
||||||
|
type ExtractedCameraComponents = (
|
||||||
|
ExtractedCamera,
|
||||||
|
ExtractedView,
|
||||||
|
RenderVisibleEntities,
|
||||||
|
TemporalJitter,
|
||||||
|
MipBias,
|
||||||
|
RenderLayers,
|
||||||
|
Projection,
|
||||||
|
NoIndirectDrawing,
|
||||||
|
ViewUniformOffset,
|
||||||
|
);
|
||||||
|
for (
|
||||||
|
main_entity,
|
||||||
|
render_entity,
|
||||||
|
camera,
|
||||||
|
render_target,
|
||||||
|
camera_render_graph,
|
||||||
|
transform,
|
||||||
|
visible_entities,
|
||||||
|
frustum,
|
||||||
|
(
|
||||||
|
hdr,
|
||||||
|
color_grading,
|
||||||
|
exposure,
|
||||||
|
temporal_jitter,
|
||||||
|
mip_bias,
|
||||||
|
render_layers,
|
||||||
|
projection,
|
||||||
|
no_indirect_drawing,
|
||||||
|
),
|
||||||
|
) in query.iter()
|
||||||
|
{
|
||||||
|
if !camera.is_active {
|
||||||
|
commands
|
||||||
|
.entity(render_entity)
|
||||||
|
.remove::<ExtractedCameraComponents>();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone();
|
||||||
|
|
||||||
|
if let (
|
||||||
|
Some(URect {
|
||||||
|
min: viewport_origin,
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
Some(viewport_size),
|
||||||
|
Some(target_size),
|
||||||
|
) = (
|
||||||
|
camera.physical_viewport_rect(),
|
||||||
|
camera.physical_viewport_size(),
|
||||||
|
camera.physical_target_size(),
|
||||||
|
) {
|
||||||
|
if target_size.x == 0 || target_size.y == 0 {
|
||||||
|
commands
|
||||||
|
.entity(render_entity)
|
||||||
|
.remove::<ExtractedCameraComponents>();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let render_visible_entities = RenderVisibleEntities {
|
||||||
|
entities: visible_entities
|
||||||
|
.entities
|
||||||
|
.iter()
|
||||||
|
.map(|(type_id, entities)| {
|
||||||
|
let entities = entities
|
||||||
|
.iter()
|
||||||
|
.map(|entity| {
|
||||||
|
let render_entity = mapper
|
||||||
|
.get(*entity)
|
||||||
|
.cloned()
|
||||||
|
.map(|entity| entity.id())
|
||||||
|
.unwrap_or(Entity::PLACEHOLDER);
|
||||||
|
(render_entity, (*entity).into())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
(*type_id, entities)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut commands = commands.entity(render_entity);
|
||||||
|
commands.insert((
|
||||||
|
ExtractedCamera {
|
||||||
|
target: render_target.normalize(primary_window),
|
||||||
|
viewport: camera.viewport.clone(),
|
||||||
|
physical_viewport_size: Some(viewport_size),
|
||||||
|
physical_target_size: Some(target_size),
|
||||||
|
render_graph: camera_render_graph.0,
|
||||||
|
order: camera.order,
|
||||||
|
output_mode: camera.output_mode,
|
||||||
|
msaa_writeback: camera.msaa_writeback,
|
||||||
|
clear_color: camera.clear_color,
|
||||||
|
// this will be set in sort_cameras
|
||||||
|
sorted_camera_index_for_target: 0,
|
||||||
|
exposure: exposure
|
||||||
|
.map(Exposure::exposure)
|
||||||
|
.unwrap_or_else(|| Exposure::default().exposure()),
|
||||||
|
hdr,
|
||||||
|
},
|
||||||
|
ExtractedView {
|
||||||
|
retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0),
|
||||||
|
clip_from_view: camera.clip_from_view(),
|
||||||
|
world_from_view: *transform,
|
||||||
|
clip_from_world: None,
|
||||||
|
hdr,
|
||||||
|
viewport: UVec4::new(
|
||||||
|
viewport_origin.x,
|
||||||
|
viewport_origin.y,
|
||||||
|
viewport_size.x,
|
||||||
|
viewport_size.y,
|
||||||
|
),
|
||||||
|
color_grading,
|
||||||
|
invert_culling: camera.invert_culling,
|
||||||
|
},
|
||||||
|
render_visible_entities,
|
||||||
|
*frustum,
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(temporal_jitter) = temporal_jitter {
|
||||||
|
commands.insert(temporal_jitter.clone());
|
||||||
|
} else {
|
||||||
|
commands.remove::<TemporalJitter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mip_bias) = mip_bias {
|
||||||
|
commands.insert(mip_bias.clone());
|
||||||
|
} else {
|
||||||
|
commands.remove::<MipBias>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(render_layers) = render_layers {
|
||||||
|
commands.insert(render_layers.clone());
|
||||||
|
} else {
|
||||||
|
commands.remove::<RenderLayers>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(projection) = projection {
|
||||||
|
commands.insert(projection.clone());
|
||||||
|
} else {
|
||||||
|
commands.remove::<Projection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if no_indirect_drawing
|
||||||
|
|| !matches!(
|
||||||
|
gpu_preprocessing_support.max_supported_mode,
|
||||||
|
GpuPreprocessingMode::Culling
|
||||||
|
)
|
||||||
|
{
|
||||||
|
commands.insert(NoIndirectDrawing);
|
||||||
|
} else {
|
||||||
|
commands.remove::<NoIndirectDrawing>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cameras sorted by their order field. This is updated in the [`sort_cameras`] system.
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct SortedCameras(pub Vec<SortedCamera>);
|
||||||
|
|
||||||
|
pub struct SortedCamera {
|
||||||
|
pub entity: Entity,
|
||||||
|
pub order: isize,
|
||||||
|
pub target: Option<NormalizedRenderTarget>,
|
||||||
|
pub hdr: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sort_cameras(
|
||||||
|
mut sorted_cameras: ResMut<SortedCameras>,
|
||||||
|
mut cameras: Query<(Entity, &mut ExtractedCamera)>,
|
||||||
|
) {
|
||||||
|
sorted_cameras.0.clear();
|
||||||
|
for (entity, camera) in cameras.iter() {
|
||||||
|
sorted_cameras.0.push(SortedCamera {
|
||||||
|
entity,
|
||||||
|
order: camera.order,
|
||||||
|
target: camera.target.clone(),
|
||||||
|
hdr: camera.hdr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// sort by order and ensure within an order, RenderTargets of the same type are packed together
|
||||||
|
sorted_cameras
|
||||||
|
.0
|
||||||
|
.sort_by(|c1, c2| (c1.order, &c1.target).cmp(&(c2.order, &c2.target)));
|
||||||
|
let mut previous_order_target = None;
|
||||||
|
let mut ambiguities = <HashSet<_>>::default();
|
||||||
|
let mut target_counts = <HashMap<_, _>>::default();
|
||||||
|
for sorted_camera in &mut sorted_cameras.0 {
|
||||||
|
let new_order_target = (sorted_camera.order, sorted_camera.target.clone());
|
||||||
|
if let Some(previous_order_target) = previous_order_target
|
||||||
|
&& previous_order_target == new_order_target
|
||||||
|
{
|
||||||
|
ambiguities.insert(new_order_target.clone());
|
||||||
|
}
|
||||||
|
if let Some(target) = &sorted_camera.target {
|
||||||
|
let count = target_counts
|
||||||
|
.entry((target.clone(), sorted_camera.hdr))
|
||||||
|
.or_insert(0usize);
|
||||||
|
let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap();
|
||||||
|
camera.sorted_camera_index_for_target = *count;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
previous_order_target = Some(new_order_target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ambiguities.is_empty() {
|
||||||
|
warn!(
|
||||||
|
"Camera order ambiguities detected for active cameras with the following priorities: {:?}. \
|
||||||
|
To fix this, ensure there is exactly one Camera entity spawned with a given order for a given RenderTarget. \
|
||||||
|
Ambiguities should be resolved because either (1) multiple active cameras were spawned accidentally, which will \
|
||||||
|
result in rendering multiple instances of the scene or (2) for cases where multiple active cameras is intentional, \
|
||||||
|
ambiguities could result in unpredictable render results.",
|
||||||
|
ambiguities
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A subpixel offset to jitter a perspective camera's frustum by.
|
||||||
|
///
|
||||||
|
/// Useful for temporal rendering techniques.
|
||||||
|
#[derive(Component, Clone, Default, Reflect)]
|
||||||
|
#[reflect(Default, Component, Clone)]
|
||||||
|
pub struct TemporalJitter {
|
||||||
|
/// Offset is in range [-0.5, 0.5].
|
||||||
|
pub offset: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemporalJitter {
|
||||||
|
pub fn jitter_projection(&self, clip_from_view: &mut Mat4, view_size: Vec2) {
|
||||||
|
// https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/docs/techniques/media/super-resolution-temporal/jitter-space.svg
|
||||||
|
let mut jitter = (self.offset * vec2(2.0, -2.0)) / view_size;
|
||||||
|
|
||||||
|
// orthographic
|
||||||
|
if clip_from_view.w_axis.w == 1.0 {
|
||||||
|
jitter *= vec2(clip_from_view.x_axis.x, clip_from_view.y_axis.y) * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
clip_from_view.z_axis.x += jitter.x;
|
||||||
|
clip_from_view.z_axis.y += jitter.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Camera component specifying a mip bias to apply when sampling from material textures.
|
||||||
|
///
|
||||||
|
/// Often used in conjunction with antialiasing post-process effects to reduce textures blurriness.
|
||||||
|
#[derive(Component, Reflect, Clone)]
|
||||||
|
#[reflect(Default, Component)]
|
||||||
|
pub struct MipBias(pub f32);
|
||||||
|
|
||||||
|
impl Default for MipBias {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(-1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
third_party/bevy_render/src/color_operations.wgsl
vendored
Normal file
47
third_party/bevy_render/src/color_operations.wgsl
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#define_import_path bevy_render::color_operations
|
||||||
|
|
||||||
|
#import bevy_render::maths::FRAC_PI_3
|
||||||
|
|
||||||
|
// Converts HSV to RGB.
|
||||||
|
//
|
||||||
|
// Input: H ∈ [0, 2π), S ∈ [0, 1], V ∈ [0, 1].
|
||||||
|
// Output: R ∈ [0, 1], G ∈ [0, 1], B ∈ [0, 1].
|
||||||
|
//
|
||||||
|
// <https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative>
|
||||||
|
fn hsv_to_rgb(hsv: vec3<f32>) -> vec3<f32> {
|
||||||
|
let n = vec3(5.0, 3.0, 1.0);
|
||||||
|
let k = (n + hsv.x / FRAC_PI_3) % 6.0;
|
||||||
|
return hsv.z - hsv.z * hsv.y * max(vec3(0.0), min(k, min(4.0 - k, vec3(1.0))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts RGB to HSV.
|
||||||
|
//
|
||||||
|
// Input: R ∈ [0, 1], G ∈ [0, 1], B ∈ [0, 1].
|
||||||
|
// Output: H ∈ [0, 2π), S ∈ [0, 1], V ∈ [0, 1].
|
||||||
|
//
|
||||||
|
// <https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB>
|
||||||
|
fn rgb_to_hsv(rgb: vec3<f32>) -> vec3<f32> {
|
||||||
|
let x_max = max(rgb.r, max(rgb.g, rgb.b)); // i.e. V
|
||||||
|
let x_min = min(rgb.r, min(rgb.g, rgb.b));
|
||||||
|
let c = x_max - x_min; // chroma
|
||||||
|
|
||||||
|
var swizzle = vec3<f32>(0.0);
|
||||||
|
if (x_max == rgb.r) {
|
||||||
|
swizzle = vec3(rgb.gb, 0.0);
|
||||||
|
} else if (x_max == rgb.g) {
|
||||||
|
swizzle = vec3(rgb.br, 2.0);
|
||||||
|
} else {
|
||||||
|
swizzle = vec3(rgb.rg, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let h = FRAC_PI_3 * (((swizzle.x - swizzle.y) / c + swizzle.z) % 6.0);
|
||||||
|
|
||||||
|
// Avoid division by zero.
|
||||||
|
var s = 0.0;
|
||||||
|
if (x_max > 0.0) {
|
||||||
|
s = c / x_max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec3(h, s, x_max);
|
||||||
|
}
|
||||||
|
|
||||||
81
third_party/bevy_render/src/diagnostic/erased_render_asset_diagnostic_plugin.rs
vendored
Normal file
81
third_party/bevy_render/src/diagnostic/erased_render_asset_diagnostic_plugin.rs
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use core::{any::type_name, marker::PhantomData};
|
||||||
|
|
||||||
|
use bevy_app::{Plugin, PreUpdate};
|
||||||
|
use bevy_diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
|
||||||
|
use bevy_ecs::{resource::Resource, system::Res};
|
||||||
|
use bevy_platform::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
erased_render_asset::{ErasedRenderAsset, ErasedRenderAssets},
|
||||||
|
Extract, ExtractSchedule, RenderApp,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Collects diagnostics for a [`ErasedRenderAsset`].
|
||||||
|
///
|
||||||
|
/// If the [`ErasedRenderAsset::ErasedAsset`] is shared between other
|
||||||
|
/// [`ErasedRenderAsset`], they all will report the same number.
|
||||||
|
pub struct ErasedRenderAssetDiagnosticPlugin<A: ErasedRenderAsset> {
|
||||||
|
suffix: &'static str,
|
||||||
|
_phantom: PhantomData<A>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: ErasedRenderAsset> ErasedRenderAssetDiagnosticPlugin<A> {
|
||||||
|
pub fn new(suffix: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
suffix,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_asset_diagnostic_path() -> DiagnosticPath {
|
||||||
|
DiagnosticPath::from_components(["erased_render_asset", type_name::<A>()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: ErasedRenderAsset> Plugin for ErasedRenderAssetDiagnosticPlugin<A> {
|
||||||
|
fn build(&self, app: &mut bevy_app::App) {
|
||||||
|
app.register_diagnostic(
|
||||||
|
Diagnostic::new(Self::render_asset_diagnostic_path()).with_suffix(self.suffix),
|
||||||
|
)
|
||||||
|
.init_resource::<ErasedRenderAssetMeasurements<A>>()
|
||||||
|
.add_systems(PreUpdate, add_erased_render_asset_measurement::<A>);
|
||||||
|
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app.add_systems(ExtractSchedule, measure_erased_render_asset::<A>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Resource)]
|
||||||
|
struct ErasedRenderAssetMeasurements<A: ErasedRenderAsset> {
|
||||||
|
assets: AtomicUsize,
|
||||||
|
_phantom: PhantomData<A>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: ErasedRenderAsset> Default for ErasedRenderAssetMeasurements<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
assets: AtomicUsize::default(),
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_erased_render_asset_measurement<A: ErasedRenderAsset>(
|
||||||
|
mut diagnostics: Diagnostics,
|
||||||
|
measurements: Res<ErasedRenderAssetMeasurements<A>>,
|
||||||
|
) {
|
||||||
|
diagnostics.add_measurement(
|
||||||
|
&ErasedRenderAssetDiagnosticPlugin::<A>::render_asset_diagnostic_path(),
|
||||||
|
|| measurements.assets.load(Ordering::Relaxed) as f64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure_erased_render_asset<A: ErasedRenderAsset>(
|
||||||
|
measurements: Extract<Res<ErasedRenderAssetMeasurements<A>>>,
|
||||||
|
assets: Res<ErasedRenderAssets<A::ErasedAsset>>,
|
||||||
|
) {
|
||||||
|
measurements
|
||||||
|
.assets
|
||||||
|
.store(assets.iter().count(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
711
third_party/bevy_render/src/diagnostic/internal.rs
vendored
Normal file
711
third_party/bevy_render/src/diagnostic/internal.rs
vendored
Normal file
@ -0,0 +1,711 @@
|
|||||||
|
use alloc::{borrow::Cow, sync::Arc};
|
||||||
|
use core::{
|
||||||
|
ops::{DerefMut, Range},
|
||||||
|
sync::atomic::{AtomicBool, Ordering},
|
||||||
|
};
|
||||||
|
use std::thread::{self, ThreadId};
|
||||||
|
|
||||||
|
use bevy_diagnostic::{Diagnostic, DiagnosticMeasurement, DiagnosticPath, DiagnosticsStore};
|
||||||
|
use bevy_ecs::resource::Resource;
|
||||||
|
use bevy_ecs::system::{Res, ResMut};
|
||||||
|
use bevy_platform::time::Instant;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use wgpu::{
|
||||||
|
Buffer, BufferDescriptor, BufferUsages, CommandEncoder, ComputePass, Features, MapMode,
|
||||||
|
PipelineStatisticsTypes, QuerySet, QuerySetDescriptor, QueryType, RenderPass,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper};
|
||||||
|
|
||||||
|
use super::RecordDiagnostics;
|
||||||
|
|
||||||
|
// buffer offset must be divisible by 256, so this constant must be divisible by 32 (=256/8)
|
||||||
|
const MAX_TIMESTAMP_QUERIES: u32 = 256;
|
||||||
|
const MAX_PIPELINE_STATISTICS: u32 = 128;
|
||||||
|
|
||||||
|
const TIMESTAMP_SIZE: u64 = 8;
|
||||||
|
const PIPELINE_STATISTICS_SIZE: u64 = 40;
|
||||||
|
|
||||||
|
struct DiagnosticsRecorderInternal {
|
||||||
|
timestamp_period_ns: f32,
|
||||||
|
features: Features,
|
||||||
|
current_frame: Mutex<FrameData>,
|
||||||
|
submitted_frames: Vec<FrameData>,
|
||||||
|
finished_frames: Vec<FrameData>,
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
tracy_gpu_context: tracy_client::GpuContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records diagnostics into [`QuerySet`]'s keeping track of the mapping between
|
||||||
|
/// spans and indices to the corresponding entries in the [`QuerySet`].
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct DiagnosticsRecorder(WgpuWrapper<DiagnosticsRecorderInternal>);
|
||||||
|
|
||||||
|
impl DiagnosticsRecorder {
|
||||||
|
/// Creates the new `DiagnosticsRecorder`.
|
||||||
|
pub fn new(
|
||||||
|
adapter_info: &RenderAdapterInfo,
|
||||||
|
device: &RenderDevice,
|
||||||
|
queue: &RenderQueue,
|
||||||
|
) -> DiagnosticsRecorder {
|
||||||
|
let features = device.features();
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
let tracy_gpu_context =
|
||||||
|
super::tracy_gpu::new_tracy_gpu_context(adapter_info, device, queue);
|
||||||
|
let _ = adapter_info; // Prevent unused variable warnings when tracing-tracy is not enabled
|
||||||
|
|
||||||
|
DiagnosticsRecorder(WgpuWrapper::new(DiagnosticsRecorderInternal {
|
||||||
|
timestamp_period_ns: queue.get_timestamp_period(),
|
||||||
|
features,
|
||||||
|
current_frame: Mutex::new(FrameData::new(
|
||||||
|
device,
|
||||||
|
features,
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
tracy_gpu_context.clone(),
|
||||||
|
)),
|
||||||
|
submitted_frames: Vec::new(),
|
||||||
|
finished_frames: Vec::new(),
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
tracy_gpu_context,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_frame_mut(&mut self) -> &mut FrameData {
|
||||||
|
self.0.current_frame.get_mut().expect("lock poisoned")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_frame_lock(&self) -> impl DerefMut<Target = FrameData> + '_ {
|
||||||
|
self.0.current_frame.lock().expect("lock poisoned")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begins recording diagnostics for a new frame.
|
||||||
|
pub fn begin_frame(&mut self) {
|
||||||
|
let internal = &mut self.0;
|
||||||
|
let mut idx = 0;
|
||||||
|
while idx < internal.submitted_frames.len() {
|
||||||
|
let timestamp = internal.timestamp_period_ns;
|
||||||
|
if internal.submitted_frames[idx].run_mapped_callback(timestamp) {
|
||||||
|
let removed = internal.submitted_frames.swap_remove(idx);
|
||||||
|
internal.finished_frames.push(removed);
|
||||||
|
} else {
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.current_frame_mut().begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies data from [`QuerySet`]'s to a [`Buffer`], after which it can be downloaded to CPU.
|
||||||
|
///
|
||||||
|
/// Should be called before [`DiagnosticsRecorder::finish_frame`].
|
||||||
|
pub fn resolve(&mut self, encoder: &mut CommandEncoder) {
|
||||||
|
self.current_frame_mut().resolve(encoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finishes recording diagnostics for the current frame.
|
||||||
|
///
|
||||||
|
/// The specified `callback` will be invoked when diagnostics become available.
|
||||||
|
///
|
||||||
|
/// Should be called after [`DiagnosticsRecorder::resolve`],
|
||||||
|
/// and **after** all commands buffers have been queued.
|
||||||
|
pub fn finish_frame(
|
||||||
|
&mut self,
|
||||||
|
device: &RenderDevice,
|
||||||
|
callback: impl FnOnce(RenderDiagnostics) + Send + Sync + 'static,
|
||||||
|
) {
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
let tracy_gpu_context = self.0.tracy_gpu_context.clone();
|
||||||
|
|
||||||
|
let internal = &mut self.0;
|
||||||
|
internal
|
||||||
|
.current_frame
|
||||||
|
.get_mut()
|
||||||
|
.expect("lock poisoned")
|
||||||
|
.finish(callback);
|
||||||
|
|
||||||
|
// reuse one of the finished frames, if we can
|
||||||
|
let new_frame = match internal.finished_frames.pop() {
|
||||||
|
Some(frame) => frame,
|
||||||
|
None => FrameData::new(
|
||||||
|
device,
|
||||||
|
internal.features,
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
tracy_gpu_context,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let old_frame = core::mem::replace(
|
||||||
|
internal.current_frame.get_mut().expect("lock poisoned"),
|
||||||
|
new_frame,
|
||||||
|
);
|
||||||
|
internal.submitted_frames.push(old_frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecordDiagnostics for DiagnosticsRecorder {
|
||||||
|
fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, span_name: Cow<'static, str>) {
|
||||||
|
self.current_frame_lock()
|
||||||
|
.begin_time_span(encoder, span_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E) {
|
||||||
|
self.current_frame_lock().end_time_span(encoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_pass_span<P: Pass>(&self, pass: &mut P, span_name: Cow<'static, str>) {
|
||||||
|
self.current_frame_lock().begin_pass(pass, span_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_pass_span<P: Pass>(&self, pass: &mut P) {
|
||||||
|
self.current_frame_lock().end_pass(pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpanRecord {
|
||||||
|
thread_id: ThreadId,
|
||||||
|
path_range: Range<usize>,
|
||||||
|
pass_kind: Option<PassKind>,
|
||||||
|
begin_timestamp_index: Option<u32>,
|
||||||
|
end_timestamp_index: Option<u32>,
|
||||||
|
begin_instant: Option<Instant>,
|
||||||
|
end_instant: Option<Instant>,
|
||||||
|
pipeline_statistics_index: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FrameData {
|
||||||
|
timestamps_query_set: Option<QuerySet>,
|
||||||
|
num_timestamps: u32,
|
||||||
|
supports_timestamps_inside_passes: bool,
|
||||||
|
supports_timestamps_inside_encoders: bool,
|
||||||
|
pipeline_statistics_query_set: Option<QuerySet>,
|
||||||
|
num_pipeline_statistics: u32,
|
||||||
|
buffer_size: u64,
|
||||||
|
pipeline_statistics_buffer_offset: u64,
|
||||||
|
resolve_buffer: Option<Buffer>,
|
||||||
|
read_buffer: Option<Buffer>,
|
||||||
|
path_components: Vec<Cow<'static, str>>,
|
||||||
|
open_spans: Vec<SpanRecord>,
|
||||||
|
closed_spans: Vec<SpanRecord>,
|
||||||
|
is_mapped: Arc<AtomicBool>,
|
||||||
|
callback: Option<Box<dyn FnOnce(RenderDiagnostics) + Send + Sync + 'static>>,
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
tracy_gpu_context: tracy_client::GpuContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameData {
|
||||||
|
fn new(
|
||||||
|
device: &RenderDevice,
|
||||||
|
features: Features,
|
||||||
|
#[cfg(feature = "tracing-tracy")] tracy_gpu_context: tracy_client::GpuContext,
|
||||||
|
) -> FrameData {
|
||||||
|
let wgpu_device = device.wgpu_device();
|
||||||
|
let mut buffer_size = 0;
|
||||||
|
|
||||||
|
let timestamps_query_set = if features.contains(Features::TIMESTAMP_QUERY) {
|
||||||
|
buffer_size += u64::from(MAX_TIMESTAMP_QUERIES) * TIMESTAMP_SIZE;
|
||||||
|
Some(wgpu_device.create_query_set(&QuerySetDescriptor {
|
||||||
|
label: Some("timestamps_query_set"),
|
||||||
|
ty: QueryType::Timestamp,
|
||||||
|
count: MAX_TIMESTAMP_QUERIES,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let pipeline_statistics_buffer_offset = buffer_size;
|
||||||
|
|
||||||
|
let pipeline_statistics_query_set =
|
||||||
|
if features.contains(Features::PIPELINE_STATISTICS_QUERY) {
|
||||||
|
buffer_size += u64::from(MAX_PIPELINE_STATISTICS) * PIPELINE_STATISTICS_SIZE;
|
||||||
|
Some(wgpu_device.create_query_set(&QuerySetDescriptor {
|
||||||
|
label: Some("pipeline_statistics_query_set"),
|
||||||
|
ty: QueryType::PipelineStatistics(PipelineStatisticsTypes::all()),
|
||||||
|
count: MAX_PIPELINE_STATISTICS,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let (resolve_buffer, read_buffer) = if buffer_size > 0 {
|
||||||
|
let resolve_buffer = wgpu_device.create_buffer(&BufferDescriptor {
|
||||||
|
label: Some("render_statistics_resolve_buffer"),
|
||||||
|
size: buffer_size,
|
||||||
|
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
let read_buffer = wgpu_device.create_buffer(&BufferDescriptor {
|
||||||
|
label: Some("render_statistics_read_buffer"),
|
||||||
|
size: buffer_size,
|
||||||
|
usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
(Some(resolve_buffer), Some(read_buffer))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
FrameData {
|
||||||
|
timestamps_query_set,
|
||||||
|
num_timestamps: 0,
|
||||||
|
supports_timestamps_inside_passes: features
|
||||||
|
.contains(Features::TIMESTAMP_QUERY_INSIDE_PASSES),
|
||||||
|
supports_timestamps_inside_encoders: features
|
||||||
|
.contains(Features::TIMESTAMP_QUERY_INSIDE_ENCODERS),
|
||||||
|
pipeline_statistics_query_set,
|
||||||
|
num_pipeline_statistics: 0,
|
||||||
|
buffer_size,
|
||||||
|
pipeline_statistics_buffer_offset,
|
||||||
|
resolve_buffer,
|
||||||
|
read_buffer,
|
||||||
|
path_components: Vec::new(),
|
||||||
|
open_spans: Vec::new(),
|
||||||
|
closed_spans: Vec::new(),
|
||||||
|
is_mapped: Arc::new(AtomicBool::new(false)),
|
||||||
|
callback: None,
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
tracy_gpu_context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin(&mut self) {
|
||||||
|
self.num_timestamps = 0;
|
||||||
|
self.num_pipeline_statistics = 0;
|
||||||
|
self.path_components.clear();
|
||||||
|
self.open_spans.clear();
|
||||||
|
self.closed_spans.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_timestamp(
|
||||||
|
&mut self,
|
||||||
|
encoder: &mut impl WriteTimestamp,
|
||||||
|
is_inside_pass: bool,
|
||||||
|
) -> Option<u32> {
|
||||||
|
// `encoder.write_timestamp` is unsupported on WebGPU.
|
||||||
|
if !self.supports_timestamps_inside_encoders {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_inside_pass && !self.supports_timestamps_inside_passes {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.num_timestamps >= MAX_TIMESTAMP_QUERIES {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let set = self.timestamps_query_set.as_ref()?;
|
||||||
|
let index = self.num_timestamps;
|
||||||
|
encoder.write_timestamp(set, index);
|
||||||
|
self.num_timestamps += 1;
|
||||||
|
Some(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_pipeline_statistics(
|
||||||
|
&mut self,
|
||||||
|
encoder: &mut impl WritePipelineStatistics,
|
||||||
|
) -> Option<u32> {
|
||||||
|
if self.num_pipeline_statistics >= MAX_PIPELINE_STATISTICS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let set = self.pipeline_statistics_query_set.as_ref()?;
|
||||||
|
let index = self.num_pipeline_statistics;
|
||||||
|
encoder.begin_pipeline_statistics_query(set, index);
|
||||||
|
self.num_pipeline_statistics += 1;
|
||||||
|
Some(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_span(
|
||||||
|
&mut self,
|
||||||
|
pass_kind: Option<PassKind>,
|
||||||
|
name: Cow<'static, str>,
|
||||||
|
) -> &mut SpanRecord {
|
||||||
|
let thread_id = thread::current().id();
|
||||||
|
|
||||||
|
let parent = self.open_spans.iter().rfind(|v| v.thread_id == thread_id);
|
||||||
|
|
||||||
|
let path_range = match &parent {
|
||||||
|
Some(parent) if parent.path_range.end == self.path_components.len() => {
|
||||||
|
parent.path_range.start..parent.path_range.end + 1
|
||||||
|
}
|
||||||
|
Some(parent) => {
|
||||||
|
self.path_components
|
||||||
|
.extend_from_within(parent.path_range.clone());
|
||||||
|
self.path_components.len() - parent.path_range.len()..self.path_components.len() + 1
|
||||||
|
}
|
||||||
|
None => self.path_components.len()..self.path_components.len() + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.path_components.push(name);
|
||||||
|
|
||||||
|
self.open_spans.push(SpanRecord {
|
||||||
|
thread_id,
|
||||||
|
path_range,
|
||||||
|
pass_kind,
|
||||||
|
begin_timestamp_index: None,
|
||||||
|
end_timestamp_index: None,
|
||||||
|
begin_instant: None,
|
||||||
|
end_instant: None,
|
||||||
|
pipeline_statistics_index: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.open_spans.last_mut().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_span(&mut self) -> &mut SpanRecord {
|
||||||
|
let thread_id = thread::current().id();
|
||||||
|
|
||||||
|
let iter = self.open_spans.iter();
|
||||||
|
let (index, _) = iter
|
||||||
|
.enumerate()
|
||||||
|
.rfind(|(_, v)| v.thread_id == thread_id)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let span = self.open_spans.swap_remove(index);
|
||||||
|
self.closed_spans.push(span);
|
||||||
|
self.closed_spans.last_mut().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_time_span(&mut self, encoder: &mut impl WriteTimestamp, name: Cow<'static, str>) {
|
||||||
|
let begin_instant = Instant::now();
|
||||||
|
let begin_timestamp_index = self.write_timestamp(encoder, false);
|
||||||
|
|
||||||
|
let span = self.open_span(None, name);
|
||||||
|
span.begin_instant = Some(begin_instant);
|
||||||
|
span.begin_timestamp_index = begin_timestamp_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_time_span(&mut self, encoder: &mut impl WriteTimestamp) {
|
||||||
|
let end_timestamp_index = self.write_timestamp(encoder, false);
|
||||||
|
|
||||||
|
let span = self.close_span();
|
||||||
|
span.end_timestamp_index = end_timestamp_index;
|
||||||
|
span.end_instant = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_pass<P: Pass>(&mut self, pass: &mut P, name: Cow<'static, str>) {
|
||||||
|
let begin_instant = Instant::now();
|
||||||
|
|
||||||
|
let begin_timestamp_index = self.write_timestamp(pass, true);
|
||||||
|
let pipeline_statistics_index = self.write_pipeline_statistics(pass);
|
||||||
|
|
||||||
|
let span = self.open_span(Some(P::KIND), name);
|
||||||
|
span.begin_instant = Some(begin_instant);
|
||||||
|
span.begin_timestamp_index = begin_timestamp_index;
|
||||||
|
span.pipeline_statistics_index = pipeline_statistics_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_pass(&mut self, pass: &mut impl Pass) {
|
||||||
|
let end_timestamp_index = self.write_timestamp(pass, true);
|
||||||
|
|
||||||
|
let span = self.close_span();
|
||||||
|
span.end_timestamp_index = end_timestamp_index;
|
||||||
|
|
||||||
|
if span.pipeline_statistics_index.is_some() {
|
||||||
|
pass.end_pipeline_statistics_query();
|
||||||
|
}
|
||||||
|
|
||||||
|
span.end_instant = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&mut self, encoder: &mut CommandEncoder) {
|
||||||
|
let Some(resolve_buffer) = &self.resolve_buffer else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match &self.timestamps_query_set {
|
||||||
|
Some(set) if self.num_timestamps > 0 => {
|
||||||
|
encoder.resolve_query_set(set, 0..self.num_timestamps, resolve_buffer, 0);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
match &self.pipeline_statistics_query_set {
|
||||||
|
Some(set) if self.num_pipeline_statistics > 0 => {
|
||||||
|
encoder.resolve_query_set(
|
||||||
|
set,
|
||||||
|
0..self.num_pipeline_statistics,
|
||||||
|
resolve_buffer,
|
||||||
|
self.pipeline_statistics_buffer_offset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(read_buffer) = &self.read_buffer else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
encoder.copy_buffer_to_buffer(resolve_buffer, 0, read_buffer, 0, self.buffer_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnostic_path(&self, range: &Range<usize>, field: &str) -> DiagnosticPath {
|
||||||
|
DiagnosticPath::from_components(
|
||||||
|
core::iter::once("render")
|
||||||
|
.chain(self.path_components[range.clone()].iter().map(|v| &**v))
|
||||||
|
.chain(core::iter::once(field)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&mut self, callback: impl FnOnce(RenderDiagnostics) + Send + Sync + 'static) {
|
||||||
|
let Some(read_buffer) = &self.read_buffer else {
|
||||||
|
// we still have cpu timings, so let's use them
|
||||||
|
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
|
||||||
|
for span in &self.closed_spans {
|
||||||
|
if let (Some(begin), Some(end)) = (span.begin_instant, span.end_instant) {
|
||||||
|
diagnostics.push(RenderDiagnostic {
|
||||||
|
path: self.diagnostic_path(&span.path_range, "elapsed_cpu"),
|
||||||
|
suffix: "ms",
|
||||||
|
value: (end - begin).as_secs_f64() * 1000.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(RenderDiagnostics(diagnostics));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.callback = Some(Box::new(callback));
|
||||||
|
|
||||||
|
let is_mapped = self.is_mapped.clone();
|
||||||
|
read_buffer.slice(..).map_async(MapMode::Read, move |res| {
|
||||||
|
if let Err(e) = res {
|
||||||
|
tracing::warn!("Failed to download render statistics buffer: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_mapped.store(true, Ordering::Release);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if the frame is considered finished, false otherwise
|
||||||
|
fn run_mapped_callback(&mut self, timestamp_period_ns: f32) -> bool {
|
||||||
|
let Some(read_buffer) = &self.read_buffer else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if !self.is_mapped.load(Ordering::Acquire) {
|
||||||
|
// need to wait more
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(callback) = self.callback.take() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = read_buffer.slice(..).get_mapped_range();
|
||||||
|
|
||||||
|
let timestamps = data[..(self.num_timestamps * 8) as usize]
|
||||||
|
.chunks(8)
|
||||||
|
.map(|v| u64::from_le_bytes(v.try_into().unwrap()))
|
||||||
|
.collect::<Vec<u64>>();
|
||||||
|
|
||||||
|
let start = self.pipeline_statistics_buffer_offset as usize;
|
||||||
|
let len = (self.num_pipeline_statistics as usize) * 40;
|
||||||
|
let pipeline_statistics = data[start..start + len]
|
||||||
|
.chunks(8)
|
||||||
|
.map(|v| u64::from_le_bytes(v.try_into().unwrap()))
|
||||||
|
.collect::<Vec<u64>>();
|
||||||
|
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
|
||||||
|
for span in &self.closed_spans {
|
||||||
|
if let (Some(begin), Some(end)) = (span.begin_instant, span.end_instant) {
|
||||||
|
diagnostics.push(RenderDiagnostic {
|
||||||
|
path: self.diagnostic_path(&span.path_range, "elapsed_cpu"),
|
||||||
|
suffix: "ms",
|
||||||
|
value: (end - begin).as_secs_f64() * 1000.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(begin), Some(end)) = (span.begin_timestamp_index, span.end_timestamp_index)
|
||||||
|
{
|
||||||
|
let begin = timestamps[begin as usize] as f64;
|
||||||
|
let end = timestamps[end as usize] as f64;
|
||||||
|
let value = (end - begin) * (timestamp_period_ns as f64) / 1e6;
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
{
|
||||||
|
// Calling span_alloc() and end_zone() here instead of in open_span() and close_span() means that tracy does not know where each GPU command was recorded on the CPU timeline.
|
||||||
|
// Unfortunately we must do it this way, because tracy does not play nicely with multithreaded command recording. The start/end pairs would get all mixed up.
|
||||||
|
// The GPU spans themselves are still accurate though, and it's probably safe to assume that each GPU span in frame N belongs to the corresponding CPU render node span from frame N-1.
|
||||||
|
let name = &self.path_components[span.path_range.clone()].join("/");
|
||||||
|
let mut tracy_gpu_span =
|
||||||
|
self.tracy_gpu_context.span_alloc(name, "", "", 0).unwrap();
|
||||||
|
tracy_gpu_span.end_zone();
|
||||||
|
tracy_gpu_span.upload_timestamp_start(begin as i64);
|
||||||
|
tracy_gpu_span.upload_timestamp_end(end as i64);
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics.push(RenderDiagnostic {
|
||||||
|
path: self.diagnostic_path(&span.path_range, "elapsed_gpu"),
|
||||||
|
suffix: "ms",
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(index) = span.pipeline_statistics_index {
|
||||||
|
let index = (index as usize) * 5;
|
||||||
|
|
||||||
|
if span.pass_kind == Some(PassKind::Render) {
|
||||||
|
diagnostics.push(RenderDiagnostic {
|
||||||
|
path: self.diagnostic_path(&span.path_range, "vertex_shader_invocations"),
|
||||||
|
suffix: "",
|
||||||
|
value: pipeline_statistics[index] as f64,
|
||||||
|
});
|
||||||
|
|
||||||
|
diagnostics.push(RenderDiagnostic {
|
||||||
|
path: self.diagnostic_path(&span.path_range, "clipper_invocations"),
|
||||||
|
suffix: "",
|
||||||
|
value: pipeline_statistics[index + 1] as f64,
|
||||||
|
});
|
||||||
|
|
||||||
|
diagnostics.push(RenderDiagnostic {
|
||||||
|
path: self.diagnostic_path(&span.path_range, "clipper_primitives_out"),
|
||||||
|
suffix: "",
|
||||||
|
value: pipeline_statistics[index + 2] as f64,
|
||||||
|
});
|
||||||
|
|
||||||
|
diagnostics.push(RenderDiagnostic {
|
||||||
|
path: self.diagnostic_path(&span.path_range, "fragment_shader_invocations"),
|
||||||
|
suffix: "",
|
||||||
|
value: pipeline_statistics[index + 3] as f64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if span.pass_kind == Some(PassKind::Compute) {
|
||||||
|
diagnostics.push(RenderDiagnostic {
|
||||||
|
path: self.diagnostic_path(&span.path_range, "compute_shader_invocations"),
|
||||||
|
suffix: "",
|
||||||
|
value: pipeline_statistics[index + 4] as f64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(RenderDiagnostics(diagnostics));
|
||||||
|
|
||||||
|
drop(data);
|
||||||
|
read_buffer.unmap();
|
||||||
|
self.is_mapped.store(false, Ordering::Release);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resource which stores render diagnostics of the most recent frame.
|
||||||
|
#[derive(Debug, Default, Clone, Resource)]
|
||||||
|
pub struct RenderDiagnostics(Vec<RenderDiagnostic>);
|
||||||
|
|
||||||
|
/// A render diagnostic which has been recorded, but not yet stored in [`DiagnosticsStore`].
|
||||||
|
#[derive(Debug, Clone, Resource)]
|
||||||
|
pub struct RenderDiagnostic {
|
||||||
|
pub path: DiagnosticPath,
|
||||||
|
pub suffix: &'static str,
|
||||||
|
pub value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores render diagnostics before they can be synced with the main app.
|
||||||
|
///
|
||||||
|
/// This mutex is locked twice per frame:
|
||||||
|
/// 1. in `PreUpdate`, during [`sync_diagnostics`],
|
||||||
|
/// 2. after rendering has finished and statistics have been downloaded from GPU.
|
||||||
|
#[derive(Debug, Default, Clone, Resource)]
|
||||||
|
pub struct RenderDiagnosticsMutex(pub(crate) Arc<Mutex<Option<RenderDiagnostics>>>);
|
||||||
|
|
||||||
|
/// Updates render diagnostics measurements.
|
||||||
|
pub fn sync_diagnostics(mutex: Res<RenderDiagnosticsMutex>, mut store: ResMut<DiagnosticsStore>) {
|
||||||
|
let Some(diagnostics) = mutex.0.lock().ok().and_then(|mut v| v.take()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let time = Instant::now();
|
||||||
|
|
||||||
|
for diagnostic in &diagnostics.0 {
|
||||||
|
if store.get(&diagnostic.path).is_none() {
|
||||||
|
store.add(Diagnostic::new(diagnostic.path.clone()).with_suffix(diagnostic.suffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
store
|
||||||
|
.get_mut(&diagnostic.path)
|
||||||
|
.unwrap()
|
||||||
|
.add_measurement(DiagnosticMeasurement {
|
||||||
|
time,
|
||||||
|
value: diagnostic.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WriteTimestamp {
|
||||||
|
fn write_timestamp(&mut self, query_set: &QuerySet, index: u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteTimestamp for CommandEncoder {
|
||||||
|
fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
// When using tracy (and thus this function), rendering was flickering on macOS Tahoe.
|
||||||
|
// See: https://github.com/bevyengine/bevy/issues/22257
|
||||||
|
// The issue seems to be triggered when `write_timestamp` is called very close to frame
|
||||||
|
// presentation.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CommandEncoder::write_timestamp(self, query_set, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteTimestamp for RenderPass<'_> {
|
||||||
|
fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) {
|
||||||
|
RenderPass::write_timestamp(self, query_set, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteTimestamp for ComputePass<'_> {
|
||||||
|
fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) {
|
||||||
|
ComputePass::write_timestamp(self, query_set, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WritePipelineStatistics {
|
||||||
|
fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32);
|
||||||
|
|
||||||
|
fn end_pipeline_statistics_query(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WritePipelineStatistics for RenderPass<'_> {
|
||||||
|
fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) {
|
||||||
|
RenderPass::begin_pipeline_statistics_query(self, query_set, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_pipeline_statistics_query(&mut self) {
|
||||||
|
RenderPass::end_pipeline_statistics_query(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WritePipelineStatistics for ComputePass<'_> {
|
||||||
|
fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) {
|
||||||
|
ComputePass::begin_pipeline_statistics_query(self, query_set, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_pipeline_statistics_query(&mut self) {
|
||||||
|
ComputePass::end_pipeline_statistics_query(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Pass: WritePipelineStatistics + WriteTimestamp {
|
||||||
|
const KIND: PassKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pass for RenderPass<'_> {
|
||||||
|
const KIND: PassKind = PassKind::Render;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pass for ComputePass<'_> {
|
||||||
|
const KIND: PassKind = PassKind::Compute;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
|
pub enum PassKind {
|
||||||
|
Render,
|
||||||
|
Compute,
|
||||||
|
}
|
||||||
91
third_party/bevy_render/src/diagnostic/mesh_allocator_diagnostic_plugin.rs
vendored
Normal file
91
third_party/bevy_render/src/diagnostic/mesh_allocator_diagnostic_plugin.rs
vendored
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
use bevy_app::{Plugin, PreUpdate};
|
||||||
|
use bevy_diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
|
||||||
|
use bevy_ecs::{resource::Resource, system::Res};
|
||||||
|
use bevy_platform::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
use crate::{mesh::allocator::MeshAllocator, Extract, ExtractSchedule, RenderApp};
|
||||||
|
|
||||||
|
/// Number of meshes allocated by the allocator
|
||||||
|
static MESH_ALLOCATOR_SLABS: DiagnosticPath = DiagnosticPath::const_new("mesh_allocator_slabs");
|
||||||
|
|
||||||
|
/// Total size of all slabs
|
||||||
|
static MESH_ALLOCATOR_SLABS_SIZE: DiagnosticPath =
|
||||||
|
DiagnosticPath::const_new("mesh_allocator_slabs_size");
|
||||||
|
|
||||||
|
/// Number of meshes allocated into slabs
|
||||||
|
static MESH_ALLOCATOR_ALLOCATIONS: DiagnosticPath =
|
||||||
|
DiagnosticPath::const_new("mesh_allocator_allocations");
|
||||||
|
|
||||||
|
pub struct MeshAllocatorDiagnosticPlugin;
|
||||||
|
|
||||||
|
impl MeshAllocatorDiagnosticPlugin {
|
||||||
|
/// Get the [`DiagnosticPath`] for slab count
|
||||||
|
pub fn slabs_diagnostic_path() -> &'static DiagnosticPath {
|
||||||
|
&MESH_ALLOCATOR_SLABS
|
||||||
|
}
|
||||||
|
/// Get the [`DiagnosticPath`] for total slabs size
|
||||||
|
pub fn slabs_size_diagnostic_path() -> &'static DiagnosticPath {
|
||||||
|
&MESH_ALLOCATOR_SLABS_SIZE
|
||||||
|
}
|
||||||
|
/// Get the [`DiagnosticPath`] for mesh allocations
|
||||||
|
pub fn allocations_diagnostic_path() -> &'static DiagnosticPath {
|
||||||
|
&MESH_ALLOCATOR_ALLOCATIONS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for MeshAllocatorDiagnosticPlugin {
|
||||||
|
fn build(&self, app: &mut bevy_app::App) {
|
||||||
|
app.register_diagnostic(
|
||||||
|
Diagnostic::new(MESH_ALLOCATOR_SLABS.clone()).with_suffix(" slabs"),
|
||||||
|
)
|
||||||
|
.register_diagnostic(
|
||||||
|
Diagnostic::new(MESH_ALLOCATOR_SLABS_SIZE.clone()).with_suffix(" bytes"),
|
||||||
|
)
|
||||||
|
.register_diagnostic(
|
||||||
|
Diagnostic::new(MESH_ALLOCATOR_ALLOCATIONS.clone()).with_suffix(" meshes"),
|
||||||
|
)
|
||||||
|
.init_resource::<MeshAllocatorMeasurements>()
|
||||||
|
.add_systems(PreUpdate, add_mesh_allocator_measurement);
|
||||||
|
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app.add_systems(ExtractSchedule, measure_allocator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Resource)]
|
||||||
|
struct MeshAllocatorMeasurements {
|
||||||
|
slabs: AtomicUsize,
|
||||||
|
slabs_size: AtomicU64,
|
||||||
|
allocations: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_mesh_allocator_measurement(
|
||||||
|
mut diagnostics: Diagnostics,
|
||||||
|
measurements: Res<MeshAllocatorMeasurements>,
|
||||||
|
) {
|
||||||
|
diagnostics.add_measurement(&MESH_ALLOCATOR_SLABS, || {
|
||||||
|
measurements.slabs.load(Ordering::Relaxed) as f64
|
||||||
|
});
|
||||||
|
diagnostics.add_measurement(&MESH_ALLOCATOR_SLABS_SIZE, || {
|
||||||
|
measurements.slabs_size.load(Ordering::Relaxed) as f64
|
||||||
|
});
|
||||||
|
diagnostics.add_measurement(&MESH_ALLOCATOR_ALLOCATIONS, || {
|
||||||
|
measurements.allocations.load(Ordering::Relaxed) as f64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure_allocator(
|
||||||
|
measurements: Extract<Res<MeshAllocatorMeasurements>>,
|
||||||
|
allocator: Res<MeshAllocator>,
|
||||||
|
) {
|
||||||
|
measurements
|
||||||
|
.slabs
|
||||||
|
.store(allocator.slab_count(), Ordering::Relaxed);
|
||||||
|
measurements
|
||||||
|
.slabs_size
|
||||||
|
.store(allocator.slabs_size(), Ordering::Relaxed);
|
||||||
|
measurements
|
||||||
|
.allocations
|
||||||
|
.store(allocator.allocations(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
199
third_party/bevy_render/src/diagnostic/mod.rs
vendored
Normal file
199
third_party/bevy_render/src/diagnostic/mod.rs
vendored
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
//! Infrastructure for recording render diagnostics.
|
||||||
|
//!
|
||||||
|
//! For more info, see [`RenderDiagnosticsPlugin`].
|
||||||
|
|
||||||
|
mod erased_render_asset_diagnostic_plugin;
|
||||||
|
pub(crate) mod internal;
|
||||||
|
mod mesh_allocator_diagnostic_plugin;
|
||||||
|
mod render_asset_diagnostic_plugin;
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
mod tracy_gpu;
|
||||||
|
|
||||||
|
use alloc::{borrow::Cow, sync::Arc};
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
use bevy_app::{App, Plugin, PreUpdate};
|
||||||
|
|
||||||
|
use crate::{renderer::RenderAdapterInfo, RenderApp};
|
||||||
|
|
||||||
|
use self::internal::{
|
||||||
|
sync_diagnostics, DiagnosticsRecorder, Pass, RenderDiagnosticsMutex, WriteTimestamp,
|
||||||
|
};
|
||||||
|
pub use self::{
|
||||||
|
erased_render_asset_diagnostic_plugin::ErasedRenderAssetDiagnosticPlugin,
|
||||||
|
mesh_allocator_diagnostic_plugin::MeshAllocatorDiagnosticPlugin,
|
||||||
|
render_asset_diagnostic_plugin::RenderAssetDiagnosticPlugin,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::renderer::{RenderDevice, RenderQueue};
|
||||||
|
|
||||||
|
/// Enables collecting render diagnostics, such as CPU/GPU elapsed time per render pass,
|
||||||
|
/// as well as pipeline statistics (number of primitives, number of shader invocations, etc).
|
||||||
|
///
|
||||||
|
/// To access the diagnostics, you can use the [`DiagnosticsStore`](bevy_diagnostic::DiagnosticsStore) resource,
|
||||||
|
/// add [`LogDiagnosticsPlugin`](bevy_diagnostic::LogDiagnosticsPlugin), or use [Tracy](https://github.com/bevyengine/bevy/blob/main/docs/profiling.md#tracy-renderqueue).
|
||||||
|
///
|
||||||
|
/// To record diagnostics in your own passes:
|
||||||
|
/// 1. First, obtain the diagnostic recorder using [`RenderContext::diagnostic_recorder`](crate::renderer::RenderContext::diagnostic_recorder).
|
||||||
|
///
|
||||||
|
/// It won't do anything unless [`RenderDiagnosticsPlugin`] is present,
|
||||||
|
/// so you're free to omit `#[cfg]` clauses.
|
||||||
|
/// ```ignore
|
||||||
|
/// let diagnostics = render_context.diagnostic_recorder();
|
||||||
|
/// ```
|
||||||
|
/// 2. Begin the span inside a command encoder, or a render/compute pass encoder.
|
||||||
|
/// ```ignore
|
||||||
|
/// let time_span = diagnostics.time_span(render_context.command_encoder(), "shadows");
|
||||||
|
/// ```
|
||||||
|
/// 3. End the span, providing the same encoder.
|
||||||
|
/// ```ignore
|
||||||
|
/// time_span.end(render_context.command_encoder());
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Supported platforms
|
||||||
|
/// Timestamp queries and pipeline statistics are currently supported only on Vulkan and DX12.
|
||||||
|
/// On other platforms (Metal, WebGPU, WebGL2) only CPU time will be recorded.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct RenderDiagnosticsPlugin;
|
||||||
|
|
||||||
|
impl Plugin for RenderDiagnosticsPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
let render_diagnostics_mutex = RenderDiagnosticsMutex::default();
|
||||||
|
app.insert_resource(render_diagnostics_mutex.clone())
|
||||||
|
.add_systems(PreUpdate, sync_diagnostics);
|
||||||
|
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app.insert_resource(render_diagnostics_mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self, app: &mut App) {
|
||||||
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let adapter_info = render_app.world().resource::<RenderAdapterInfo>();
|
||||||
|
let device = render_app.world().resource::<RenderDevice>();
|
||||||
|
let queue = render_app.world().resource::<RenderQueue>();
|
||||||
|
render_app.insert_resource(DiagnosticsRecorder::new(adapter_info, device, queue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows recording diagnostic spans.
|
||||||
|
pub trait RecordDiagnostics: Send + Sync {
|
||||||
|
/// Begin a time span, which will record elapsed CPU and GPU time.
|
||||||
|
///
|
||||||
|
/// Returns a guard, which will panic on drop unless you end the span.
|
||||||
|
fn time_span<E, N>(&self, encoder: &mut E, name: N) -> TimeSpanGuard<'_, Self, E>
|
||||||
|
where
|
||||||
|
E: WriteTimestamp,
|
||||||
|
N: Into<Cow<'static, str>>,
|
||||||
|
{
|
||||||
|
self.begin_time_span(encoder, name.into());
|
||||||
|
TimeSpanGuard {
|
||||||
|
recorder: self,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begin a pass span, which will record elapsed CPU and GPU time,
|
||||||
|
/// as well as pipeline statistics on supported platforms.
|
||||||
|
///
|
||||||
|
/// Returns a guard, which will panic on drop unless you end the span.
|
||||||
|
fn pass_span<P, N>(&self, pass: &mut P, name: N) -> PassSpanGuard<'_, Self, P>
|
||||||
|
where
|
||||||
|
P: Pass,
|
||||||
|
N: Into<Cow<'static, str>>,
|
||||||
|
{
|
||||||
|
let name = name.into();
|
||||||
|
self.begin_pass_span(pass, name.clone());
|
||||||
|
PassSpanGuard {
|
||||||
|
recorder: self,
|
||||||
|
name,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, name: Cow<'static, str>);
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E);
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn begin_pass_span<P: Pass>(&self, pass: &mut P, name: Cow<'static, str>);
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn end_pass_span<P: Pass>(&self, pass: &mut P);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard returned by [`RecordDiagnostics::time_span`].
|
||||||
|
///
|
||||||
|
/// Will panic on drop unless [`TimeSpanGuard::end`] is called.
|
||||||
|
pub struct TimeSpanGuard<'a, R: ?Sized, E> {
|
||||||
|
recorder: &'a R,
|
||||||
|
marker: PhantomData<E>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: RecordDiagnostics + ?Sized, E: WriteTimestamp> TimeSpanGuard<'_, R, E> {
|
||||||
|
/// End the span. You have to provide the same encoder which was used to begin the span.
|
||||||
|
pub fn end(self, encoder: &mut E) {
|
||||||
|
self.recorder.end_time_span(encoder);
|
||||||
|
core::mem::forget(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: ?Sized, E> Drop for TimeSpanGuard<'_, R, E> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
panic!("TimeSpanScope::end was never called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard returned by [`RecordDiagnostics::pass_span`].
|
||||||
|
///
|
||||||
|
/// Will panic on drop unless [`PassSpanGuard::end`] is called.
|
||||||
|
pub struct PassSpanGuard<'a, R: ?Sized, P> {
|
||||||
|
recorder: &'a R,
|
||||||
|
name: Cow<'static, str>,
|
||||||
|
marker: PhantomData<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: RecordDiagnostics + ?Sized, P: Pass> PassSpanGuard<'_, R, P> {
|
||||||
|
/// End the span. You have to provide the same pass which was used to begin the span.
|
||||||
|
pub fn end(self, pass: &mut P) {
|
||||||
|
self.recorder.end_pass_span(pass);
|
||||||
|
core::mem::forget(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: ?Sized, P> Drop for PassSpanGuard<'_, R, P> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
panic!("PassSpanGuard::end was never called for {}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: RecordDiagnostics> RecordDiagnostics for Option<Arc<T>> {
|
||||||
|
fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, name: Cow<'static, str>) {
|
||||||
|
if let Some(recorder) = &self {
|
||||||
|
recorder.begin_time_span(encoder, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E) {
|
||||||
|
if let Some(recorder) = &self {
|
||||||
|
recorder.end_time_span(encoder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_pass_span<P: Pass>(&self, pass: &mut P, name: Cow<'static, str>) {
|
||||||
|
if let Some(recorder) = &self {
|
||||||
|
recorder.begin_pass_span(pass, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_pass_span<P: Pass>(&self, pass: &mut P) {
|
||||||
|
if let Some(recorder) = &self {
|
||||||
|
recorder.end_pass_span(pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
third_party/bevy_render/src/diagnostic/render_asset_diagnostic_plugin.rs
vendored
Normal file
77
third_party/bevy_render/src/diagnostic/render_asset_diagnostic_plugin.rs
vendored
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use core::{any::type_name, marker::PhantomData};
|
||||||
|
|
||||||
|
use bevy_app::{Plugin, PreUpdate};
|
||||||
|
use bevy_diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
|
||||||
|
use bevy_ecs::{resource::Resource, system::Res};
|
||||||
|
use bevy_platform::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
render_asset::{RenderAsset, RenderAssets},
|
||||||
|
Extract, ExtractSchedule, RenderApp,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RenderAssetDiagnosticPlugin<A: RenderAsset> {
|
||||||
|
suffix: &'static str,
|
||||||
|
_phantom: PhantomData<A>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset> RenderAssetDiagnosticPlugin<A> {
|
||||||
|
pub fn new(suffix: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
suffix,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_asset_diagnostic_path() -> DiagnosticPath {
|
||||||
|
DiagnosticPath::from_components(["render_asset", type_name::<A>()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset> Plugin for RenderAssetDiagnosticPlugin<A> {
|
||||||
|
fn build(&self, app: &mut bevy_app::App) {
|
||||||
|
app.register_diagnostic(
|
||||||
|
Diagnostic::new(Self::render_asset_diagnostic_path()).with_suffix(self.suffix),
|
||||||
|
)
|
||||||
|
.init_resource::<RenderAssetMeasurements<A>>()
|
||||||
|
.add_systems(PreUpdate, add_render_asset_measurement::<A>);
|
||||||
|
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app.add_systems(ExtractSchedule, measure_render_asset::<A>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Resource)]
|
||||||
|
struct RenderAssetMeasurements<A: RenderAsset> {
|
||||||
|
assets: AtomicUsize,
|
||||||
|
_phantom: PhantomData<A>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset> Default for RenderAssetMeasurements<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
assets: AtomicUsize::default(),
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_render_asset_measurement<A: RenderAsset>(
|
||||||
|
mut diagnostics: Diagnostics,
|
||||||
|
measurements: Res<RenderAssetMeasurements<A>>,
|
||||||
|
) {
|
||||||
|
diagnostics.add_measurement(
|
||||||
|
&RenderAssetDiagnosticPlugin::<A>::render_asset_diagnostic_path(),
|
||||||
|
|| measurements.assets.load(Ordering::Relaxed) as f64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure_render_asset<A: RenderAsset>(
|
||||||
|
measurements: Extract<Res<RenderAssetMeasurements<A>>>,
|
||||||
|
assets: Res<RenderAssets<A>>,
|
||||||
|
) {
|
||||||
|
measurements
|
||||||
|
.assets
|
||||||
|
.store(assets.iter().count(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
69
third_party/bevy_render/src/diagnostic/tracy_gpu.rs
vendored
Normal file
69
third_party/bevy_render/src/diagnostic/tracy_gpu.rs
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use crate::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue};
|
||||||
|
use tracy_client::{Client, GpuContext, GpuContextType};
|
||||||
|
use wgpu::{
|
||||||
|
Backend, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, MapMode, PollType,
|
||||||
|
QuerySetDescriptor, QueryType, QUERY_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn new_tracy_gpu_context(
|
||||||
|
adapter_info: &RenderAdapterInfo,
|
||||||
|
device: &RenderDevice,
|
||||||
|
queue: &RenderQueue,
|
||||||
|
) -> GpuContext {
|
||||||
|
let tracy_gpu_backend = match adapter_info.backend {
|
||||||
|
Backend::Vulkan => GpuContextType::Vulkan,
|
||||||
|
Backend::Dx12 => GpuContextType::Direct3D12,
|
||||||
|
Backend::Gl => GpuContextType::OpenGL,
|
||||||
|
Backend::Metal | Backend::BrowserWebGpu | Backend::Noop => GpuContextType::Invalid,
|
||||||
|
};
|
||||||
|
|
||||||
|
let tracy_client = Client::running().unwrap();
|
||||||
|
tracy_client
|
||||||
|
.new_gpu_context(
|
||||||
|
Some("RenderQueue"),
|
||||||
|
tracy_gpu_backend,
|
||||||
|
initial_timestamp(device, queue),
|
||||||
|
queue.get_timestamp_period(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code copied from https://github.com/Wumpf/wgpu-profiler/blob/f9de342a62cb75f50904a98d11dd2bbeb40ceab8/src/tracy.rs
|
||||||
|
fn initial_timestamp(device: &RenderDevice, queue: &RenderQueue) -> i64 {
|
||||||
|
let query_set = device.wgpu_device().create_query_set(&QuerySetDescriptor {
|
||||||
|
label: None,
|
||||||
|
ty: QueryType::Timestamp,
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resolve_buffer = device.create_buffer(&BufferDescriptor {
|
||||||
|
label: None,
|
||||||
|
size: QUERY_SIZE as _,
|
||||||
|
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let map_buffer = device.create_buffer(&BufferDescriptor {
|
||||||
|
label: None,
|
||||||
|
size: QUERY_SIZE as _,
|
||||||
|
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut timestamp_encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||||
|
timestamp_encoder.write_timestamp(&query_set, 0);
|
||||||
|
timestamp_encoder.resolve_query_set(&query_set, 0..1, &resolve_buffer, 0);
|
||||||
|
// Workaround for https://github.com/gfx-rs/wgpu/issues/6406
|
||||||
|
// TODO when that bug is fixed, merge these encoders together again
|
||||||
|
let mut copy_encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||||
|
copy_encoder.copy_buffer_to_buffer(&resolve_buffer, 0, &map_buffer, 0, Some(QUERY_SIZE as _));
|
||||||
|
queue.submit([timestamp_encoder.finish(), copy_encoder.finish()]);
|
||||||
|
|
||||||
|
map_buffer.slice(..).map_async(MapMode::Read, |_| ());
|
||||||
|
device
|
||||||
|
.poll(PollType::wait_indefinitely())
|
||||||
|
.expect("Failed to poll device for map async");
|
||||||
|
|
||||||
|
let view = map_buffer.slice(..).get_mapped_range();
|
||||||
|
i64::from_le_bytes((*view).try_into().unwrap())
|
||||||
|
}
|
||||||
427
third_party/bevy_render/src/erased_render_asset.rs
vendored
Normal file
427
third_party/bevy_render/src/erased_render_asset.rs
vendored
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
use crate::{
|
||||||
|
render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp,
|
||||||
|
RenderSystems, Res,
|
||||||
|
};
|
||||||
|
use bevy_app::{App, Plugin, SubApp};
|
||||||
|
use bevy_asset::RenderAssetUsages;
|
||||||
|
use bevy_asset::{Asset, AssetEvent, AssetId, Assets, UntypedAssetId};
|
||||||
|
use bevy_ecs::{
|
||||||
|
prelude::{Commands, IntoScheduleConfigs, MessageReader, ResMut, Resource},
|
||||||
|
schedule::{ScheduleConfigs, SystemSet},
|
||||||
|
system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState},
|
||||||
|
world::{FromWorld, Mut},
|
||||||
|
};
|
||||||
|
use bevy_platform::collections::{HashMap, HashSet};
|
||||||
|
use bevy_render::render_asset::RenderAssetBytesPerFrameLimiter;
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PrepareAssetError<E: Send + Sync + 'static> {
|
||||||
|
#[error("Failed to prepare asset")]
|
||||||
|
RetryNextUpdate(E),
|
||||||
|
#[error("Failed to build bind group: {0}")]
|
||||||
|
AsBindGroupError(AsBindGroupError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The system set during which we extract modified assets to the render world.
|
||||||
|
#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
|
||||||
|
pub struct AssetExtractionSystems;
|
||||||
|
|
||||||
|
/// Describes how an asset gets extracted and prepared for rendering.
|
||||||
|
///
|
||||||
|
/// In the [`ExtractSchedule`] step the [`ErasedRenderAsset::SourceAsset`] is transferred
|
||||||
|
/// from the "main world" into the "render world".
|
||||||
|
///
|
||||||
|
/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset
|
||||||
|
/// is transformed into its GPU-representation of type [`ErasedRenderAsset`].
|
||||||
|
pub trait ErasedRenderAsset: Send + Sync + 'static {
|
||||||
|
/// The representation of the asset in the "main world".
|
||||||
|
type SourceAsset: Asset + Clone;
|
||||||
|
/// The target representation of the asset in the "render world".
|
||||||
|
type ErasedAsset: Send + Sync + 'static + Sized;
|
||||||
|
|
||||||
|
/// Specifies all ECS data required by [`ErasedRenderAsset::prepare_asset`].
|
||||||
|
///
|
||||||
|
/// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`].
|
||||||
|
type Param: SystemParam;
|
||||||
|
|
||||||
|
/// Whether or not to unload the asset after extracting it to the render world.
|
||||||
|
#[inline]
|
||||||
|
fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages {
|
||||||
|
RenderAssetUsages::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size of the data the asset will upload to the gpu. Specifying a return value
|
||||||
|
/// will allow the asset to be throttled via [`RenderAssetBytesPerFrameLimiter`].
|
||||||
|
#[inline]
|
||||||
|
#[expect(
|
||||||
|
unused_variables,
|
||||||
|
reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion."
|
||||||
|
)]
|
||||||
|
fn byte_len(erased_asset: &Self::SourceAsset) -> Option<usize> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares the [`ErasedRenderAsset::SourceAsset`] for the GPU by transforming it into a [`ErasedRenderAsset`].
|
||||||
|
///
|
||||||
|
/// ECS data may be accessed via `param`.
|
||||||
|
fn prepare_asset(
|
||||||
|
source_asset: Self::SourceAsset,
|
||||||
|
asset_id: AssetId<Self::SourceAsset>,
|
||||||
|
param: &mut SystemParamItem<Self::Param>,
|
||||||
|
) -> Result<Self::ErasedAsset, PrepareAssetError<Self::SourceAsset>>;
|
||||||
|
|
||||||
|
/// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed.
|
||||||
|
///
|
||||||
|
/// You can implement this method if you need to access ECS data (via
|
||||||
|
/// `_param`) in order to perform cleanup tasks when the asset is removed.
|
||||||
|
///
|
||||||
|
/// The default implementation does nothing.
|
||||||
|
fn unload_asset(
|
||||||
|
_source_asset: AssetId<Self::SourceAsset>,
|
||||||
|
_param: &mut SystemParamItem<Self::Param>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This plugin extracts the changed assets from the "app world" into the "render world"
|
||||||
|
/// and prepares them for the GPU. They can then be accessed from the [`ErasedRenderAssets`] resource.
|
||||||
|
///
|
||||||
|
/// Therefore it sets up the [`ExtractSchedule`] and
|
||||||
|
/// [`RenderSystems::PrepareAssets`] steps for the specified [`ErasedRenderAsset`].
|
||||||
|
///
|
||||||
|
/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until
|
||||||
|
/// `prepare_assets::<AFTER>` has completed. This allows the `prepare_asset` function to depend on another
|
||||||
|
/// prepared [`ErasedRenderAsset`], for example `Mesh::prepare_asset` relies on `ErasedRenderAssets::<GpuImage>` for morph
|
||||||
|
/// targets, so the plugin is created as `ErasedRenderAssetPlugin::<RenderMesh, GpuImage>::default()`.
|
||||||
|
pub struct ErasedRenderAssetPlugin<
|
||||||
|
A: ErasedRenderAsset,
|
||||||
|
AFTER: ErasedRenderAssetDependency + 'static = (),
|
||||||
|
> {
|
||||||
|
phantom: PhantomData<fn() -> (A, AFTER)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: ErasedRenderAsset, AFTER: ErasedRenderAssetDependency + 'static> Default
|
||||||
|
for ErasedRenderAssetPlugin<A, AFTER>
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
phantom: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: ErasedRenderAsset, AFTER: ErasedRenderAssetDependency + 'static> Plugin
|
||||||
|
for ErasedRenderAssetPlugin<A, AFTER>
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<CachedExtractErasedRenderAssetSystemState<A>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self, app: &mut App) {
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app
|
||||||
|
.init_resource::<ExtractedAssets<A>>()
|
||||||
|
.init_resource::<ErasedRenderAssets<A::ErasedAsset>>()
|
||||||
|
.init_resource::<PrepareNextFrameAssets<A>>()
|
||||||
|
.add_systems(
|
||||||
|
ExtractSchedule,
|
||||||
|
extract_erased_render_asset::<A>.in_set(AssetExtractionSystems),
|
||||||
|
);
|
||||||
|
AFTER::register_system(
|
||||||
|
render_app,
|
||||||
|
prepare_erased_assets::<A>.in_set(RenderSystems::PrepareAssets),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to allow specifying dependencies between render assets
|
||||||
|
pub trait ErasedRenderAssetDependency {
|
||||||
|
fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErasedRenderAssetDependency for () {
|
||||||
|
fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
|
||||||
|
render_app.add_systems(Render, system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: ErasedRenderAsset> ErasedRenderAssetDependency for A {
|
||||||
|
fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
|
||||||
|
render_app.add_systems(Render, system.after(prepare_erased_assets::<A>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporarily stores the extracted and removed assets of the current frame.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ExtractedAssets<A: ErasedRenderAsset> {
|
||||||
|
/// The assets extracted this frame.
|
||||||
|
///
|
||||||
|
/// These are assets that were either added or modified this frame.
|
||||||
|
pub extracted: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
|
||||||
|
|
||||||
|
/// IDs of the assets that were removed this frame.
|
||||||
|
///
|
||||||
|
/// These assets will not be present in [`ExtractedAssets::extracted`].
|
||||||
|
pub removed: HashSet<AssetId<A::SourceAsset>>,
|
||||||
|
|
||||||
|
/// IDs of the assets that were modified this frame.
|
||||||
|
pub modified: HashSet<AssetId<A::SourceAsset>>,
|
||||||
|
|
||||||
|
/// IDs of the assets that were added this frame.
|
||||||
|
pub added: HashSet<AssetId<A::SourceAsset>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: ErasedRenderAsset> Default for ExtractedAssets<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
extracted: Default::default(),
|
||||||
|
removed: Default::default(),
|
||||||
|
modified: Default::default(),
|
||||||
|
added: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores all GPU representations ([`ErasedRenderAsset`])
|
||||||
|
/// of [`ErasedRenderAsset::SourceAsset`] as long as they exist.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ErasedRenderAssets<ERA>(HashMap<UntypedAssetId, ERA>);
|
||||||
|
|
||||||
|
impl<ERA> Default for ErasedRenderAssets<ERA> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ERA> ErasedRenderAssets<ERA> {
|
||||||
|
pub fn get(&self, id: impl Into<UntypedAssetId>) -> Option<&ERA> {
|
||||||
|
self.0.get(&id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, id: impl Into<UntypedAssetId>) -> Option<&mut ERA> {
|
||||||
|
self.0.get_mut(&id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, id: impl Into<UntypedAssetId>, value: ERA) -> Option<ERA> {
|
||||||
|
self.0.insert(id.into(), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, id: impl Into<UntypedAssetId>) -> Option<ERA> {
|
||||||
|
self.0.remove(&id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (UntypedAssetId, &ERA)> {
|
||||||
|
self.0.iter().map(|(k, v)| (*k, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_mut(&mut self) -> impl Iterator<Item = (UntypedAssetId, &mut ERA)> {
|
||||||
|
self.0.iter_mut().map(|(k, v)| (*k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct CachedExtractErasedRenderAssetSystemState<A: ErasedRenderAsset> {
|
||||||
|
state: SystemState<(
|
||||||
|
MessageReader<'static, 'static, AssetEvent<A::SourceAsset>>,
|
||||||
|
ResMut<'static, Assets<A::SourceAsset>>,
|
||||||
|
)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: ErasedRenderAsset> FromWorld for CachedExtractErasedRenderAssetSystemState<A> {
|
||||||
|
fn from_world(world: &mut bevy_ecs::world::World) -> Self {
|
||||||
|
Self {
|
||||||
|
state: SystemState::new(world),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system extracts all created or modified assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type
|
||||||
|
/// into the "render world".
|
||||||
|
pub(crate) fn extract_erased_render_asset<A: ErasedRenderAsset>(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut main_world: ResMut<MainWorld>,
|
||||||
|
) {
|
||||||
|
main_world.resource_scope(
|
||||||
|
|world, mut cached_state: Mut<CachedExtractErasedRenderAssetSystemState<A>>| {
|
||||||
|
let (mut events, mut assets) = cached_state.state.get_mut(world);
|
||||||
|
|
||||||
|
let mut needs_extracting = <HashSet<_>>::default();
|
||||||
|
let mut removed = <HashSet<_>>::default();
|
||||||
|
let mut modified = <HashSet<_>>::default();
|
||||||
|
|
||||||
|
for event in events.read() {
|
||||||
|
#[expect(
|
||||||
|
clippy::match_same_arms,
|
||||||
|
reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."
|
||||||
|
)]
|
||||||
|
match event {
|
||||||
|
AssetEvent::Added { id } => {
|
||||||
|
needs_extracting.insert(*id);
|
||||||
|
}
|
||||||
|
AssetEvent::Modified { id } => {
|
||||||
|
needs_extracting.insert(*id);
|
||||||
|
modified.insert(*id);
|
||||||
|
}
|
||||||
|
AssetEvent::Removed { .. } => {
|
||||||
|
// We don't care that the asset was removed from Assets<T> in the main world.
|
||||||
|
// An asset is only removed from ErasedRenderAssets<T> when its last handle is dropped (AssetEvent::Unused).
|
||||||
|
}
|
||||||
|
AssetEvent::Unused { id } => {
|
||||||
|
needs_extracting.remove(id);
|
||||||
|
modified.remove(id);
|
||||||
|
removed.insert(*id);
|
||||||
|
}
|
||||||
|
AssetEvent::LoadedWithDependencies { .. } => {
|
||||||
|
// TODO: handle this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut extracted_assets = Vec::new();
|
||||||
|
let mut added = <HashSet<_>>::default();
|
||||||
|
for id in needs_extracting.drain() {
|
||||||
|
if let Some(asset) = assets.get(id) {
|
||||||
|
let asset_usage = A::asset_usage(asset);
|
||||||
|
if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) {
|
||||||
|
if asset_usage == RenderAssetUsages::RENDER_WORLD {
|
||||||
|
if let Some(asset) = assets.remove(id) {
|
||||||
|
extracted_assets.push((id, asset));
|
||||||
|
added.insert(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extracted_assets.push((id, asset.clone()));
|
||||||
|
added.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.insert_resource(ExtractedAssets::<A> {
|
||||||
|
extracted: extracted_assets,
|
||||||
|
removed,
|
||||||
|
modified,
|
||||||
|
added,
|
||||||
|
});
|
||||||
|
cached_state.state.apply(world);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: consider storing inside system?
|
||||||
|
/// All assets that should be prepared next frame.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct PrepareNextFrameAssets<A: ErasedRenderAsset> {
|
||||||
|
assets: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: ErasedRenderAsset> Default for PrepareNextFrameAssets<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
assets: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system prepares all assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type
|
||||||
|
/// which where extracted this frame for the GPU.
|
||||||
|
pub fn prepare_erased_assets<A: ErasedRenderAsset>(
|
||||||
|
mut extracted_assets: ResMut<ExtractedAssets<A>>,
|
||||||
|
mut render_assets: ResMut<ErasedRenderAssets<A::ErasedAsset>>,
|
||||||
|
mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,
|
||||||
|
param: StaticSystemParam<<A as ErasedRenderAsset>::Param>,
|
||||||
|
bpf: Res<RenderAssetBytesPerFrameLimiter>,
|
||||||
|
) {
|
||||||
|
let mut wrote_asset_count = 0;
|
||||||
|
|
||||||
|
let mut param = param.into_inner();
|
||||||
|
let queued_assets = core::mem::take(&mut prepare_next_frame.assets);
|
||||||
|
for (id, extracted_asset) in queued_assets {
|
||||||
|
if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) {
|
||||||
|
// skip previous frame's assets that have been removed or updated
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
|
||||||
|
// we could check if available bytes > byte_len here, but we want to make some
|
||||||
|
// forward progress even if the asset is larger than the max bytes per frame.
|
||||||
|
// this way we always write at least one (sized) asset per frame.
|
||||||
|
// in future we could also consider partial asset uploads.
|
||||||
|
if bpf.exhausted() {
|
||||||
|
prepare_next_frame.assets.push((id, extracted_asset));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
match A::prepare_asset(extracted_asset, id, &mut param) {
|
||||||
|
Ok(prepared_asset) => {
|
||||||
|
render_assets.insert(id, prepared_asset);
|
||||||
|
bpf.write_bytes(write_bytes);
|
||||||
|
wrote_asset_count += 1;
|
||||||
|
}
|
||||||
|
Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
|
||||||
|
prepare_next_frame.assets.push((id, extracted_asset));
|
||||||
|
}
|
||||||
|
Err(PrepareAssetError::AsBindGroupError(e)) => {
|
||||||
|
error!(
|
||||||
|
"{} Bind group construction failed: {e}",
|
||||||
|
core::any::type_name::<A>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for removed in extracted_assets.removed.drain() {
|
||||||
|
render_assets.remove(removed);
|
||||||
|
A::unload_asset(removed, &mut param);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (id, extracted_asset) in extracted_assets.extracted.drain(..) {
|
||||||
|
// we remove previous here to ensure that if we are updating the asset then
|
||||||
|
// any users will not see the old asset after a new asset is extracted,
|
||||||
|
// even if the new asset is not yet ready or we are out of bytes to write.
|
||||||
|
render_assets.remove(id);
|
||||||
|
|
||||||
|
let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
|
||||||
|
if bpf.exhausted() {
|
||||||
|
prepare_next_frame.assets.push((id, extracted_asset));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
match A::prepare_asset(extracted_asset, id, &mut param) {
|
||||||
|
Ok(prepared_asset) => {
|
||||||
|
render_assets.insert(id, prepared_asset);
|
||||||
|
bpf.write_bytes(write_bytes);
|
||||||
|
wrote_asset_count += 1;
|
||||||
|
}
|
||||||
|
Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
|
||||||
|
prepare_next_frame.assets.push((id, extracted_asset));
|
||||||
|
}
|
||||||
|
Err(PrepareAssetError::AsBindGroupError(e)) => {
|
||||||
|
error!(
|
||||||
|
"{} Bind group construction failed: {e}",
|
||||||
|
core::any::type_name::<A>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bpf.exhausted() && !prepare_next_frame.assets.is_empty() {
|
||||||
|
debug!(
|
||||||
|
"{} write budget exhausted with {} assets remaining (wrote {})",
|
||||||
|
core::any::type_name::<A>(),
|
||||||
|
prepare_next_frame.assets.len(),
|
||||||
|
wrote_asset_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
third_party/bevy_render/src/experimental/mod.rs
vendored
Normal file
6
third_party/bevy_render/src/experimental/mod.rs
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
//! Experimental rendering features.
|
||||||
|
//!
|
||||||
|
//! Experimental features are features with known problems, but are included
|
||||||
|
//! nonetheless for testing purposes.
|
||||||
|
|
||||||
|
pub mod occlusion_culling;
|
||||||
69
third_party/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl
vendored
Normal file
69
third_party/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Types needed for GPU mesh uniform building.
|
||||||
|
|
||||||
|
#define_import_path bevy_pbr::mesh_preprocess_types
|
||||||
|
|
||||||
|
// Per-frame data that the CPU supplies to the GPU.
|
||||||
|
struct MeshInput {
|
||||||
|
// The model transform.
|
||||||
|
world_from_local: mat3x4<f32>,
|
||||||
|
// The lightmap UV rect, packed into 64 bits.
|
||||||
|
lightmap_uv_rect: vec2<u32>,
|
||||||
|
// Various flags.
|
||||||
|
flags: u32,
|
||||||
|
previous_input_index: u32,
|
||||||
|
first_vertex_index: u32,
|
||||||
|
first_index_index: u32,
|
||||||
|
index_count: u32,
|
||||||
|
current_skin_index: u32,
|
||||||
|
// Low 16 bits: index of the material inside the bind group data.
|
||||||
|
// High 16 bits: index of the lightmap in the binding array.
|
||||||
|
material_and_lightmap_bind_group_slot: u32,
|
||||||
|
timestamp: u32,
|
||||||
|
// User supplied index to identify the mesh instance
|
||||||
|
tag: u32,
|
||||||
|
pad: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `wgpu` indirect parameters structure. This is a union of two structures.
|
||||||
|
// For more information, see the corresponding comment in
|
||||||
|
// `gpu_preprocessing.rs`.
|
||||||
|
struct IndirectParametersIndexed {
|
||||||
|
// `vertex_count` or `index_count`.
|
||||||
|
index_count: u32,
|
||||||
|
// `instance_count` in both structures.
|
||||||
|
instance_count: u32,
|
||||||
|
// `first_vertex` or `first_index`.
|
||||||
|
first_index: u32,
|
||||||
|
// `base_vertex` or `first_instance`.
|
||||||
|
base_vertex: u32,
|
||||||
|
// A read-only copy of `instance_index`.
|
||||||
|
first_instance: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IndirectParametersNonIndexed {
|
||||||
|
vertex_count: u32,
|
||||||
|
instance_count: u32,
|
||||||
|
base_vertex: u32,
|
||||||
|
first_instance: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IndirectParametersCpuMetadata {
|
||||||
|
base_output_index: u32,
|
||||||
|
batch_set_index: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IndirectParametersGpuMetadata {
|
||||||
|
mesh_index: u32,
|
||||||
|
#ifdef WRITE_INDIRECT_PARAMETERS_METADATA
|
||||||
|
early_instance_count: atomic<u32>,
|
||||||
|
late_instance_count: atomic<u32>,
|
||||||
|
#else // WRITE_INDIRECT_PARAMETERS_METADATA
|
||||||
|
early_instance_count: u32,
|
||||||
|
late_instance_count: u32,
|
||||||
|
#endif // WRITE_INDIRECT_PARAMETERS_METADATA
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IndirectBatchSet {
|
||||||
|
indirect_parameters_count: atomic<u32>,
|
||||||
|
indirect_parameters_base: u32,
|
||||||
|
}
|
||||||
104
third_party/bevy_render/src/experimental/occlusion_culling/mod.rs
vendored
Normal file
104
third_party/bevy_render/src/experimental/occlusion_culling/mod.rs
vendored
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
//! GPU occlusion culling.
|
||||||
|
//!
|
||||||
|
//! See [`OcclusionCulling`] for a detailed description of occlusion culling in
|
||||||
|
//! Bevy.
|
||||||
|
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_ecs::{component::Component, entity::Entity, prelude::ReflectComponent};
|
||||||
|
use bevy_reflect::{prelude::ReflectDefault, Reflect};
|
||||||
|
use bevy_shader::load_shader_library;
|
||||||
|
|
||||||
|
use crate::{extract_component::ExtractComponent, render_resource::TextureView};
|
||||||
|
|
||||||
|
/// Enables GPU occlusion culling.
|
||||||
|
///
|
||||||
|
/// See [`OcclusionCulling`] for a detailed description of occlusion culling in
|
||||||
|
/// Bevy.
|
||||||
|
pub struct OcclusionCullingPlugin;
|
||||||
|
|
||||||
|
impl Plugin for OcclusionCullingPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
load_shader_library!(app, "mesh_preprocess_types.wgsl");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add this component to a view in order to enable experimental GPU occlusion
|
||||||
|
/// culling.
|
||||||
|
///
|
||||||
|
/// *Bevy's occlusion culling is currently marked as experimental.* There are
|
||||||
|
/// known issues whereby, in rare circumstances, occlusion culling can result in
|
||||||
|
/// meshes being culled that shouldn't be (i.e. meshes that turn invisible).
|
||||||
|
/// Please try it out and report issues.
|
||||||
|
///
|
||||||
|
/// *Occlusion culling* allows Bevy to avoid rendering objects that are fully
|
||||||
|
/// behind other opaque or alpha tested objects. This is different from, and
|
||||||
|
/// complements, depth fragment rejection as the `DepthPrepass` enables. While
|
||||||
|
/// depth rejection allows Bevy to avoid rendering *pixels* that are behind
|
||||||
|
/// other objects, the GPU still has to examine those pixels to reject them,
|
||||||
|
/// which requires transforming the vertices of the objects and performing
|
||||||
|
/// skinning if the objects were skinned. Occlusion culling allows the GPU to go
|
||||||
|
/// a step further, avoiding even transforming the vertices of objects that it
|
||||||
|
/// can quickly prove to be behind other objects.
|
||||||
|
///
|
||||||
|
/// Occlusion culling inherently has some overhead, because Bevy must examine
|
||||||
|
/// the objects' bounding boxes, and create an acceleration structure
|
||||||
|
/// (hierarchical Z-buffer) to perform the occlusion tests. Therefore, occlusion
|
||||||
|
/// culling is disabled by default. Only enable it if you measure it to be a
|
||||||
|
/// speedup on your scene. Note that, because Bevy's occlusion culling runs on
|
||||||
|
/// the GPU and is quite efficient, it's rare for occlusion culling to result in
|
||||||
|
/// a significant slowdown.
|
||||||
|
///
|
||||||
|
/// Occlusion culling currently requires a `DepthPrepass`. If no depth prepass
|
||||||
|
/// is present on the view, the [`OcclusionCulling`] component will be ignored.
|
||||||
|
/// Additionally, occlusion culling is currently incompatible with deferred
|
||||||
|
/// shading; including both `DeferredPrepass` and [`OcclusionCulling`] results
|
||||||
|
/// in unspecified behavior.
|
||||||
|
///
|
||||||
|
/// The algorithm that Bevy uses is known as [*two-phase occlusion culling*].
|
||||||
|
/// When you enable occlusion culling, Bevy splits the depth prepass into two:
|
||||||
|
/// an *early* depth prepass and a *late* depth prepass. The early depth prepass
|
||||||
|
/// renders all the meshes that were visible last frame to produce a
|
||||||
|
/// conservative approximation of the depth buffer. Then, after producing an
|
||||||
|
/// acceleration structure known as a hierarchical Z-buffer or depth pyramid,
|
||||||
|
/// Bevy tests the bounding boxes of all meshes against that depth buffer. Those
|
||||||
|
/// that can be quickly proven to be behind the geometry rendered during the
|
||||||
|
/// early depth prepass are skipped entirely. The other potentially-visible
|
||||||
|
/// meshes are rendered during the late prepass, and finally all the visible
|
||||||
|
/// meshes are rendered as usual during the opaque, transparent, etc. passes.
|
||||||
|
///
|
||||||
|
/// Unlike other occlusion culling systems you may be familiar with, Bevy's
|
||||||
|
/// occlusion culling is fully dynamic and requires no baking step. The CPU
|
||||||
|
/// overhead is minimal. Large skinned meshes and other dynamic objects can
|
||||||
|
/// occlude other objects.
|
||||||
|
///
|
||||||
|
/// [*two-phase occlusion culling*]:
|
||||||
|
/// https://medium.com/@mil_kru/two-pass-occlusion-culling-4100edcad501
|
||||||
|
#[derive(Component, ExtractComponent, Clone, Copy, Default, Reflect)]
|
||||||
|
#[reflect(Component, Default, Clone)]
|
||||||
|
pub struct OcclusionCulling;
|
||||||
|
|
||||||
|
/// A render-world component that contains resources necessary to perform
|
||||||
|
/// occlusion culling on any view other than a camera.
|
||||||
|
///
|
||||||
|
/// Bevy automatically places this component on views created for shadow
|
||||||
|
/// mapping. You don't ordinarily need to add this component yourself.
|
||||||
|
#[derive(Clone, Component)]
|
||||||
|
pub struct OcclusionCullingSubview {
|
||||||
|
/// A texture view of the Z-buffer.
|
||||||
|
pub depth_texture_view: TextureView,
|
||||||
|
/// The size of the texture along both dimensions.
|
||||||
|
///
|
||||||
|
/// Because [`OcclusionCullingSubview`] is only currently used for shadow
|
||||||
|
/// maps, they're guaranteed to have sizes equal to a power of two, so we
|
||||||
|
/// don't have to store the two dimensions individually here.
|
||||||
|
pub depth_texture_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A render-world component placed on each camera that stores references to all
|
||||||
|
/// entities other than cameras that need occlusion culling.
|
||||||
|
///
|
||||||
|
/// Bevy automatically places this component on cameras that are drawing
|
||||||
|
/// shadows, when those shadows come from lights with occlusion culling enabled.
|
||||||
|
/// You don't ordinarily need to add this component yourself.
|
||||||
|
#[derive(Clone, Component)]
|
||||||
|
pub struct OcclusionCullingSubviewEntities(pub Vec<Entity>);
|
||||||
236
third_party/bevy_render/src/extract_component.rs
vendored
Normal file
236
third_party/bevy_render/src/extract_component.rs
vendored
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
use crate::{
|
||||||
|
render_resource::{encase::internal::WriteInto, DynamicUniformBuffer, ShaderType},
|
||||||
|
renderer::{RenderDevice, RenderQueue},
|
||||||
|
sync_component::SyncComponentPlugin,
|
||||||
|
sync_world::RenderEntity,
|
||||||
|
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||||
|
};
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_camera::visibility::ViewVisibility;
|
||||||
|
use bevy_ecs::{
|
||||||
|
bundle::NoBundleEffect,
|
||||||
|
component::Component,
|
||||||
|
prelude::*,
|
||||||
|
query::{QueryFilter, QueryItem, ReadOnlyQueryData},
|
||||||
|
};
|
||||||
|
use core::{marker::PhantomData, ops::Deref};
|
||||||
|
|
||||||
|
pub use bevy_render_macros::ExtractComponent;
|
||||||
|
|
||||||
|
/// Stores the index of a uniform inside of [`ComponentUniforms`].
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct DynamicUniformIndex<C: Component> {
|
||||||
|
index: u32,
|
||||||
|
marker: PhantomData<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component> DynamicUniformIndex<C> {
|
||||||
|
#[inline]
|
||||||
|
pub fn index(&self) -> u32 {
|
||||||
|
self.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how a component gets extracted for rendering.
|
||||||
|
///
|
||||||
|
/// Therefore the component is transferred from the "app world" into the "render world"
|
||||||
|
/// in the [`ExtractSchedule`] step.
|
||||||
|
pub trait ExtractComponent: Component {
|
||||||
|
/// ECS [`ReadOnlyQueryData`] to fetch the components to extract.
|
||||||
|
type QueryData: ReadOnlyQueryData;
|
||||||
|
/// Filters the entities with additional constraints.
|
||||||
|
type QueryFilter: QueryFilter;
|
||||||
|
|
||||||
|
/// The output from extraction.
|
||||||
|
///
|
||||||
|
/// Returning `None` based on the queried item will remove the component from the entity in
|
||||||
|
/// the render world. This can be used, for example, to conditionally extract camera settings
|
||||||
|
/// in order to disable a rendering feature on the basis of those settings, without removing
|
||||||
|
/// the component from the entity in the main world.
|
||||||
|
///
|
||||||
|
/// The output may be different from the queried component.
|
||||||
|
/// This can be useful for example if only a subset of the fields are useful
|
||||||
|
/// in the render world.
|
||||||
|
///
|
||||||
|
/// `Out` has a [`Bundle`] trait bound instead of a [`Component`] trait bound in order to allow use cases
|
||||||
|
/// such as tuples of components as output.
|
||||||
|
type Out: Bundle<Effect: NoBundleEffect>;
|
||||||
|
|
||||||
|
// TODO: https://github.com/rust-lang/rust/issues/29661
|
||||||
|
// type Out: Component = Self;
|
||||||
|
|
||||||
|
/// Defines how the component is transferred into the "render world".
|
||||||
|
fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This plugin prepares the components of the corresponding type for the GPU
|
||||||
|
/// by transforming them into uniforms.
|
||||||
|
///
|
||||||
|
/// They can then be accessed from the [`ComponentUniforms`] resource.
|
||||||
|
/// For referencing the newly created uniforms a [`DynamicUniformIndex`] is inserted
|
||||||
|
/// for every processed entity.
|
||||||
|
///
|
||||||
|
/// Therefore it sets up the [`RenderSystems::Prepare`] step
|
||||||
|
/// for the specified [`ExtractComponent`].
|
||||||
|
pub struct UniformComponentPlugin<C>(PhantomData<fn() -> C>);
|
||||||
|
|
||||||
|
impl<C> Default for UniformComponentPlugin<C> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(PhantomData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component + ShaderType + WriteInto + Clone> Plugin for UniformComponentPlugin<C> {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app
|
||||||
|
.insert_resource(ComponentUniforms::<C>::default())
|
||||||
|
.add_systems(
|
||||||
|
Render,
|
||||||
|
prepare_uniform_components::<C>.in_set(RenderSystems::PrepareResources),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores all uniforms of the component type.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ComponentUniforms<C: Component + ShaderType> {
|
||||||
|
uniforms: DynamicUniformBuffer<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component + ShaderType> Deref for ComponentUniforms<C> {
|
||||||
|
type Target = DynamicUniformBuffer<C>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.uniforms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component + ShaderType> ComponentUniforms<C> {
|
||||||
|
#[inline]
|
||||||
|
pub fn uniforms(&self) -> &DynamicUniformBuffer<C> {
|
||||||
|
&self.uniforms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component + ShaderType> Default for ComponentUniforms<C> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
uniforms: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system prepares all components of the corresponding component type.
|
||||||
|
/// They are transformed into uniforms and stored in the [`ComponentUniforms`] resource.
|
||||||
|
fn prepare_uniform_components<C>(
|
||||||
|
mut commands: Commands,
|
||||||
|
render_device: Res<RenderDevice>,
|
||||||
|
render_queue: Res<RenderQueue>,
|
||||||
|
mut component_uniforms: ResMut<ComponentUniforms<C>>,
|
||||||
|
components: Query<(Entity, &C)>,
|
||||||
|
) where
|
||||||
|
C: Component + ShaderType + WriteInto + Clone,
|
||||||
|
{
|
||||||
|
let components_iter = components.iter();
|
||||||
|
let count = components_iter.len();
|
||||||
|
let Some(mut writer) =
|
||||||
|
component_uniforms
|
||||||
|
.uniforms
|
||||||
|
.get_writer(count, &render_device, &render_queue)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let entities = components_iter
|
||||||
|
.map(|(entity, component)| {
|
||||||
|
(
|
||||||
|
entity,
|
||||||
|
DynamicUniformIndex::<C> {
|
||||||
|
index: writer.write(component),
|
||||||
|
marker: PhantomData,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
commands.try_insert_batch(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This plugin extracts the components into the render world for synced entities.
|
||||||
|
///
|
||||||
|
/// To do so, it sets up the [`ExtractSchedule`] step for the specified [`ExtractComponent`].
|
||||||
|
pub struct ExtractComponentPlugin<C, F = ()> {
|
||||||
|
only_extract_visible: bool,
|
||||||
|
marker: PhantomData<fn() -> (C, F)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C, F> Default for ExtractComponentPlugin<C, F> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
only_extract_visible: false,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C, F> ExtractComponentPlugin<C, F> {
|
||||||
|
pub fn extract_visible() -> Self {
|
||||||
|
Self {
|
||||||
|
only_extract_visible: true,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: ExtractComponent> Plugin for ExtractComponentPlugin<C> {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(SyncComponentPlugin::<C>::default());
|
||||||
|
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
if self.only_extract_visible {
|
||||||
|
render_app.add_systems(ExtractSchedule, extract_visible_components::<C>);
|
||||||
|
} else {
|
||||||
|
render_app.add_systems(ExtractSchedule, extract_components::<C>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are synced via [`crate::sync_world::SyncToRenderWorld`].
|
||||||
|
fn extract_components<C: ExtractComponent>(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut previous_len: Local<usize>,
|
||||||
|
query: Extract<Query<(RenderEntity, C::QueryData), C::QueryFilter>>,
|
||||||
|
) {
|
||||||
|
let mut values = Vec::with_capacity(*previous_len);
|
||||||
|
for (entity, query_item) in &query {
|
||||||
|
if let Some(component) = C::extract_component(query_item) {
|
||||||
|
values.push((entity, component));
|
||||||
|
} else {
|
||||||
|
commands.entity(entity).remove::<C::Out>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*previous_len = values.len();
|
||||||
|
commands.try_insert_batch(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are visible and synced via [`crate::sync_world::SyncToRenderWorld`].
|
||||||
|
fn extract_visible_components<C: ExtractComponent>(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut previous_len: Local<usize>,
|
||||||
|
query: Extract<Query<(RenderEntity, &ViewVisibility, C::QueryData), C::QueryFilter>>,
|
||||||
|
) {
|
||||||
|
let mut values = Vec::with_capacity(*previous_len);
|
||||||
|
for (entity, view_visibility, query_item) in &query {
|
||||||
|
if view_visibility.get() {
|
||||||
|
if let Some(component) = C::extract_component(query_item) {
|
||||||
|
values.push((entity, component));
|
||||||
|
} else {
|
||||||
|
commands.entity(entity).remove::<C::Out>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*previous_len = values.len();
|
||||||
|
commands.try_insert_batch(values);
|
||||||
|
}
|
||||||
137
third_party/bevy_render/src/extract_instances.rs
vendored
Normal file
137
third_party/bevy_render/src/extract_instances.rs
vendored
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
//! Convenience logic for turning components from the main world into extracted
|
||||||
|
//! instances in the render world.
|
||||||
|
//!
|
||||||
|
//! This is essentially the same as the `extract_component` module, but
|
||||||
|
//! higher-performance because it avoids the ECS overhead.
|
||||||
|
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_camera::visibility::ViewVisibility;
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::{
|
||||||
|
prelude::Entity,
|
||||||
|
query::{QueryFilter, QueryItem, ReadOnlyQueryData},
|
||||||
|
resource::Resource,
|
||||||
|
system::{Query, ResMut},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::sync_world::MainEntityHashMap;
|
||||||
|
use crate::{Extract, ExtractSchedule, RenderApp};
|
||||||
|
|
||||||
|
/// Describes how to extract data needed for rendering from a component or
|
||||||
|
/// components.
|
||||||
|
///
|
||||||
|
/// Before rendering, any applicable components will be transferred from the
|
||||||
|
/// main world to the render world in the [`ExtractSchedule`] step.
|
||||||
|
///
|
||||||
|
/// This is essentially the same as
|
||||||
|
/// [`ExtractComponent`](crate::extract_component::ExtractComponent), but
|
||||||
|
/// higher-performance because it avoids the ECS overhead.
|
||||||
|
pub trait ExtractInstance: Send + Sync + Sized + 'static {
|
||||||
|
/// ECS [`ReadOnlyQueryData`] to fetch the components to extract.
|
||||||
|
type QueryData: ReadOnlyQueryData;
|
||||||
|
/// Filters the entities with additional constraints.
|
||||||
|
type QueryFilter: QueryFilter;
|
||||||
|
|
||||||
|
/// Defines how the component is transferred into the "render world".
|
||||||
|
fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This plugin extracts one or more components into the "render world" as
|
||||||
|
/// extracted instances.
|
||||||
|
///
|
||||||
|
/// Therefore it sets up the [`ExtractSchedule`] step for the specified
|
||||||
|
/// [`ExtractedInstances`].
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ExtractInstancesPlugin<EI>
|
||||||
|
where
|
||||||
|
EI: ExtractInstance,
|
||||||
|
{
|
||||||
|
only_extract_visible: bool,
|
||||||
|
marker: PhantomData<fn() -> EI>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores all extract instances of a type in the render world.
|
||||||
|
#[derive(Resource, Deref, DerefMut)]
|
||||||
|
pub struct ExtractedInstances<EI>(MainEntityHashMap<EI>)
|
||||||
|
where
|
||||||
|
EI: ExtractInstance;
|
||||||
|
|
||||||
|
impl<EI> Default for ExtractedInstances<EI>
|
||||||
|
where
|
||||||
|
EI: ExtractInstance,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<EI> ExtractInstancesPlugin<EI>
|
||||||
|
where
|
||||||
|
EI: ExtractInstance,
|
||||||
|
{
|
||||||
|
/// Creates a new [`ExtractInstancesPlugin`] that unconditionally extracts to
|
||||||
|
/// the render world, whether the entity is visible or not.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
only_extract_visible: false,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`ExtractInstancesPlugin`] that extracts to the render world
|
||||||
|
/// if and only if the entity it's attached to is visible.
|
||||||
|
pub fn extract_visible() -> Self {
|
||||||
|
Self {
|
||||||
|
only_extract_visible: true,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<EI> Plugin for ExtractInstancesPlugin<EI>
|
||||||
|
where
|
||||||
|
EI: ExtractInstance,
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app.init_resource::<ExtractedInstances<EI>>();
|
||||||
|
if self.only_extract_visible {
|
||||||
|
render_app.add_systems(ExtractSchedule, extract_visible::<EI>);
|
||||||
|
} else {
|
||||||
|
render_app.add_systems(ExtractSchedule, extract_all::<EI>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_all<EI>(
|
||||||
|
mut extracted_instances: ResMut<ExtractedInstances<EI>>,
|
||||||
|
query: Extract<Query<(Entity, EI::QueryData), EI::QueryFilter>>,
|
||||||
|
) where
|
||||||
|
EI: ExtractInstance,
|
||||||
|
{
|
||||||
|
extracted_instances.clear();
|
||||||
|
for (entity, other) in &query {
|
||||||
|
if let Some(extract_instance) = EI::extract(other) {
|
||||||
|
extracted_instances.insert(entity.into(), extract_instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_visible<EI>(
|
||||||
|
mut extracted_instances: ResMut<ExtractedInstances<EI>>,
|
||||||
|
query: Extract<Query<(Entity, &ViewVisibility, EI::QueryData), EI::QueryFilter>>,
|
||||||
|
) where
|
||||||
|
EI: ExtractInstance,
|
||||||
|
{
|
||||||
|
extracted_instances.clear();
|
||||||
|
for (entity, view_visibility, other) in &query {
|
||||||
|
if view_visibility.get()
|
||||||
|
&& let Some(extract_instance) = EI::extract(other)
|
||||||
|
{
|
||||||
|
extracted_instances.insert(entity.into(), extract_instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
177
third_party/bevy_render/src/extract_param.rs
vendored
Normal file
177
third_party/bevy_render/src/extract_param.rs
vendored
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
use crate::MainWorld;
|
||||||
|
use bevy_ecs::{
|
||||||
|
change_detection::Tick,
|
||||||
|
prelude::*,
|
||||||
|
query::FilteredAccessSet,
|
||||||
|
system::{
|
||||||
|
ReadOnlySystemParam, SystemMeta, SystemParam, SystemParamItem, SystemParamValidationError,
|
||||||
|
SystemState,
|
||||||
|
},
|
||||||
|
world::unsafe_world_cell::UnsafeWorldCell,
|
||||||
|
};
|
||||||
|
use core::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
/// A helper for accessing [`MainWorld`] content using a system parameter.
|
||||||
|
///
|
||||||
|
/// A [`SystemParam`] adapter which applies the contained `SystemParam` to the [`World`]
|
||||||
|
/// contained in [`MainWorld`]. This parameter only works for systems run
|
||||||
|
/// during the [`ExtractSchedule`](crate::ExtractSchedule).
|
||||||
|
///
|
||||||
|
/// This requires that the contained [`SystemParam`] does not mutate the world, as it
|
||||||
|
/// uses a read-only reference to [`MainWorld`] internally.
|
||||||
|
///
|
||||||
|
/// ## Context
|
||||||
|
///
|
||||||
|
/// [`ExtractSchedule`] is used to extract (move) data from the simulation world ([`MainWorld`]) to the
|
||||||
|
/// render world. The render world drives rendering each frame (generally to a `Window`).
|
||||||
|
/// This design is used to allow performing calculations related to rendering a prior frame at the same
|
||||||
|
/// time as the next frame is simulated, which increases throughput (FPS).
|
||||||
|
///
|
||||||
|
/// [`Extract`] is used to get data from the main world during [`ExtractSchedule`].
|
||||||
|
///
|
||||||
|
/// ## Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use bevy_ecs::prelude::*;
|
||||||
|
/// use bevy_render::Extract;
|
||||||
|
/// use bevy_render::sync_world::RenderEntity;
|
||||||
|
/// # #[derive(Component)]
|
||||||
|
/// // Do make sure to sync the cloud entities before extracting them.
|
||||||
|
/// # struct Cloud;
|
||||||
|
/// fn extract_clouds(mut commands: Commands, clouds: Extract<Query<RenderEntity, With<Cloud>>>) {
|
||||||
|
/// for cloud in &clouds {
|
||||||
|
/// commands.entity(cloud).insert(Cloud);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`ExtractSchedule`]: crate::ExtractSchedule
|
||||||
|
/// [Window]: bevy_window::Window
|
||||||
|
pub struct Extract<'w, 's, P>
|
||||||
|
where
|
||||||
|
P: ReadOnlySystemParam + 'static,
|
||||||
|
{
|
||||||
|
item: SystemParamItem<'w, 's, P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct ExtractState<P: SystemParam + 'static> {
|
||||||
|
state: SystemState<P>,
|
||||||
|
main_world_state: <Res<'static, MainWorld> as SystemParam>::State,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: The only `World` access (`Res<MainWorld>`) is read-only.
|
||||||
|
unsafe impl<P> ReadOnlySystemParam for Extract<'_, '_, P> where P: ReadOnlySystemParam {}
|
||||||
|
|
||||||
|
// SAFETY: The only `World` access is properly registered by `Res<MainWorld>::init_state`.
|
||||||
|
// This call will also ensure that there are no conflicts with prior params.
|
||||||
|
unsafe impl<P> SystemParam for Extract<'_, '_, P>
|
||||||
|
where
|
||||||
|
P: ReadOnlySystemParam,
|
||||||
|
{
|
||||||
|
type State = ExtractState<P>;
|
||||||
|
type Item<'w, 's> = Extract<'w, 's, P>;
|
||||||
|
|
||||||
|
fn init_state(world: &mut World) -> Self::State {
|
||||||
|
let mut main_world = world.resource_mut::<MainWorld>();
|
||||||
|
ExtractState {
|
||||||
|
state: SystemState::new(&mut main_world),
|
||||||
|
main_world_state: Res::<MainWorld>::init_state(world),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_access(
|
||||||
|
state: &Self::State,
|
||||||
|
system_meta: &mut SystemMeta,
|
||||||
|
component_access_set: &mut FilteredAccessSet,
|
||||||
|
world: &mut World,
|
||||||
|
) {
|
||||||
|
Res::<MainWorld>::init_access(
|
||||||
|
&state.main_world_state,
|
||||||
|
system_meta,
|
||||||
|
component_access_set,
|
||||||
|
world,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
unsafe fn validate_param(
|
||||||
|
state: &mut Self::State,
|
||||||
|
_system_meta: &SystemMeta,
|
||||||
|
world: UnsafeWorldCell,
|
||||||
|
) -> Result<(), SystemParamValidationError> {
|
||||||
|
// SAFETY: Read-only access to world data registered in `init_state`.
|
||||||
|
let result = unsafe { world.get_resource_by_id(state.main_world_state) };
|
||||||
|
let Some(main_world) = result else {
|
||||||
|
return Err(SystemParamValidationError::invalid::<Self>(
|
||||||
|
"`MainWorld` resource does not exist",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
// SAFETY: Type is guaranteed by `SystemState`.
|
||||||
|
let main_world: &World = unsafe { main_world.deref() };
|
||||||
|
// SAFETY: We provide the main world on which this system state was initialized on.
|
||||||
|
unsafe {
|
||||||
|
SystemState::<P>::validate_param(
|
||||||
|
&mut state.state,
|
||||||
|
main_world.as_unsafe_world_cell_readonly(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
unsafe fn get_param<'w, 's>(
|
||||||
|
state: &'s mut Self::State,
|
||||||
|
system_meta: &SystemMeta,
|
||||||
|
world: UnsafeWorldCell<'w>,
|
||||||
|
change_tick: Tick,
|
||||||
|
) -> Self::Item<'w, 's> {
|
||||||
|
// SAFETY:
|
||||||
|
// - The caller ensures that `world` is the same one that `init_state` was called with.
|
||||||
|
// - The caller ensures that no other `SystemParam`s will conflict with the accesses we have registered.
|
||||||
|
let main_world = unsafe {
|
||||||
|
Res::<MainWorld>::get_param(
|
||||||
|
&mut state.main_world_state,
|
||||||
|
system_meta,
|
||||||
|
world,
|
||||||
|
change_tick,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let item = state.state.get(main_world.into_inner());
|
||||||
|
Extract { item }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'w, 's, P> Deref for Extract<'w, 's, P>
|
||||||
|
where
|
||||||
|
P: ReadOnlySystemParam,
|
||||||
|
{
|
||||||
|
type Target = SystemParamItem<'w, 's, P>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'w, 's, P> DerefMut for Extract<'w, 's, P>
|
||||||
|
where
|
||||||
|
P: ReadOnlySystemParam,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'w, 's, P> IntoIterator for &'a Extract<'w, 's, P>
|
||||||
|
where
|
||||||
|
P: ReadOnlySystemParam,
|
||||||
|
&'a SystemParamItem<'w, 's, P>: IntoIterator,
|
||||||
|
{
|
||||||
|
type Item = <&'a SystemParamItem<'w, 's, P> as IntoIterator>::Item;
|
||||||
|
type IntoIter = <&'a SystemParamItem<'w, 's, P> as IntoIterator>::IntoIter;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
(&self.item).into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
70
third_party/bevy_render/src/extract_resource.rs
vendored
Normal file
70
third_party/bevy_render/src/extract_resource.rs
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
pub use bevy_render_macros::ExtractResource;
|
||||||
|
use bevy_utils::once;
|
||||||
|
|
||||||
|
use crate::{Extract, ExtractSchedule, RenderApp};
|
||||||
|
|
||||||
|
/// Describes how a resource gets extracted for rendering.
|
||||||
|
///
|
||||||
|
/// Therefore the resource is transferred from the "main world" into the "render world"
|
||||||
|
/// in the [`ExtractSchedule`] step.
|
||||||
|
pub trait ExtractResource: Resource {
|
||||||
|
type Source: Resource;
|
||||||
|
|
||||||
|
/// Defines how the resource is transferred into the "render world".
|
||||||
|
fn extract_resource(source: &Self::Source) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This plugin extracts the resources into the "render world".
|
||||||
|
///
|
||||||
|
/// Therefore it sets up the[`ExtractSchedule`] step
|
||||||
|
/// for the specified [`Resource`].
|
||||||
|
pub struct ExtractResourcePlugin<R: ExtractResource>(PhantomData<R>);
|
||||||
|
|
||||||
|
impl<R: ExtractResource> Default for ExtractResourcePlugin<R> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(PhantomData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: ExtractResource> Plugin for ExtractResourcePlugin<R> {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app.add_systems(ExtractSchedule, extract_resource::<R>);
|
||||||
|
} else {
|
||||||
|
once!(tracing::error!(
|
||||||
|
"Render app did not exist when trying to add `extract_resource` for <{}>.",
|
||||||
|
core::any::type_name::<R>()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system extracts the resource of the corresponding [`Resource`] type
|
||||||
|
pub fn extract_resource<R: ExtractResource>(
|
||||||
|
mut commands: Commands,
|
||||||
|
main_resource: Extract<Option<Res<R::Source>>>,
|
||||||
|
target_resource: Option<ResMut<R>>,
|
||||||
|
) {
|
||||||
|
if let Some(main_resource) = main_resource.as_ref() {
|
||||||
|
if let Some(mut target_resource) = target_resource {
|
||||||
|
if main_resource.is_changed() {
|
||||||
|
*target_resource = R::extract_resource(main_resource);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if !main_resource.is_added() {
|
||||||
|
once!(tracing::warn!(
|
||||||
|
"Removing resource {} from render world not expected, adding using `Commands`.
|
||||||
|
This may decrease performance",
|
||||||
|
core::any::type_name::<R>()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.insert_resource(R::extract_resource(main_resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
third_party/bevy_render/src/globals.rs
vendored
Normal file
79
third_party/bevy_render/src/globals.rs
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
use crate::{
|
||||||
|
extract_resource::ExtractResource,
|
||||||
|
render_resource::{ShaderType, UniformBuffer},
|
||||||
|
renderer::{RenderDevice, RenderQueue},
|
||||||
|
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||||
|
};
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_diagnostic::FrameCount;
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_reflect::prelude::*;
|
||||||
|
use bevy_shader::load_shader_library;
|
||||||
|
use bevy_time::Time;
|
||||||
|
|
||||||
|
pub struct GlobalsPlugin;
|
||||||
|
|
||||||
|
impl Plugin for GlobalsPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
load_shader_library!(app, "globals.wgsl");
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app
|
||||||
|
.init_resource::<GlobalsBuffer>()
|
||||||
|
.init_resource::<Time>()
|
||||||
|
.add_systems(ExtractSchedule, (extract_frame_count, extract_time))
|
||||||
|
.add_systems(
|
||||||
|
Render,
|
||||||
|
prepare_globals_buffer.in_set(RenderSystems::PrepareResources),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_frame_count(mut commands: Commands, frame_count: Extract<Res<FrameCount>>) {
|
||||||
|
commands.insert_resource(**frame_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_time(mut commands: Commands, time: Extract<Res<Time>>) {
|
||||||
|
commands.insert_resource(**time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains global values useful when writing shaders.
|
||||||
|
/// Currently only contains values related to time.
|
||||||
|
#[derive(Default, Clone, Resource, ExtractResource, Reflect, ShaderType)]
|
||||||
|
#[reflect(Resource, Default, Clone)]
|
||||||
|
pub struct GlobalsUniform {
|
||||||
|
/// The time since startup in seconds.
|
||||||
|
/// Wraps to 0 after 1 hour.
|
||||||
|
time: f32,
|
||||||
|
/// The delta time since the previous frame in seconds
|
||||||
|
delta_time: f32,
|
||||||
|
/// Frame count since the start of the app.
|
||||||
|
/// It wraps to zero when it reaches the maximum value of a u32.
|
||||||
|
frame_count: u32,
|
||||||
|
/// WebGL2 structs must be 16 byte aligned.
|
||||||
|
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
|
||||||
|
_wasm_padding: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The buffer containing the [`GlobalsUniform`]
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct GlobalsBuffer {
|
||||||
|
pub buffer: UniformBuffer<GlobalsUniform>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_globals_buffer(
|
||||||
|
render_device: Res<RenderDevice>,
|
||||||
|
render_queue: Res<RenderQueue>,
|
||||||
|
mut globals_buffer: ResMut<GlobalsBuffer>,
|
||||||
|
time: Res<Time>,
|
||||||
|
frame_count: Res<FrameCount>,
|
||||||
|
) {
|
||||||
|
let buffer = globals_buffer.buffer.get_mut();
|
||||||
|
buffer.time = time.elapsed_secs_wrapped();
|
||||||
|
buffer.delta_time = time.delta_secs();
|
||||||
|
buffer.frame_count = frame_count.0;
|
||||||
|
|
||||||
|
globals_buffer
|
||||||
|
.buffer
|
||||||
|
.write_buffer(&render_device, &render_queue);
|
||||||
|
}
|
||||||
16
third_party/bevy_render/src/globals.wgsl
vendored
Normal file
16
third_party/bevy_render/src/globals.wgsl
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#define_import_path bevy_render::globals
|
||||||
|
|
||||||
|
struct Globals {
|
||||||
|
// The time since startup in seconds
|
||||||
|
// Wraps to 0 after 1 hour.
|
||||||
|
time: f32,
|
||||||
|
// The delta time since the previous frame in seconds
|
||||||
|
delta_time: f32,
|
||||||
|
// Frame count since the start of the app.
|
||||||
|
// It wraps to zero when it reaches the maximum value of a u32.
|
||||||
|
frame_count: u32,
|
||||||
|
#ifdef SIXTEEN_BYTE_ALIGNMENT
|
||||||
|
// WebGL2 structs must be 16 byte aligned.
|
||||||
|
_webgl2_padding: f32
|
||||||
|
#endif
|
||||||
|
};
|
||||||
59
third_party/bevy_render/src/gpu_component_array_buffer.rs
vendored
Normal file
59
third_party/bevy_render/src/gpu_component_array_buffer.rs
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
use crate::{
|
||||||
|
render_resource::{GpuArrayBuffer, GpuArrayBufferable},
|
||||||
|
renderer::{RenderDevice, RenderQueue},
|
||||||
|
Render, RenderApp, RenderSystems,
|
||||||
|
};
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_ecs::{
|
||||||
|
prelude::{Component, Entity},
|
||||||
|
schedule::IntoScheduleConfigs,
|
||||||
|
system::{Commands, Query, Res, ResMut},
|
||||||
|
};
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
/// This plugin prepares the components of the corresponding type for the GPU
|
||||||
|
/// by storing them in a [`GpuArrayBuffer`].
|
||||||
|
pub struct GpuComponentArrayBufferPlugin<C: Component + GpuArrayBufferable>(PhantomData<C>);
|
||||||
|
|
||||||
|
impl<C: Component + GpuArrayBufferable> Plugin for GpuComponentArrayBufferPlugin<C> {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app.add_systems(
|
||||||
|
Render,
|
||||||
|
prepare_gpu_component_array_buffers::<C>.in_set(RenderSystems::PrepareResources),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self, app: &mut App) {
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app.insert_resource(GpuArrayBuffer::<C>::new(
|
||||||
|
&render_app.world().resource::<RenderDevice>().limits(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Component + GpuArrayBufferable> Default for GpuComponentArrayBufferPlugin<C> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(PhantomData::<C>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_gpu_component_array_buffers<C: Component + GpuArrayBufferable>(
|
||||||
|
mut commands: Commands,
|
||||||
|
render_device: Res<RenderDevice>,
|
||||||
|
render_queue: Res<RenderQueue>,
|
||||||
|
mut gpu_array_buffer: ResMut<GpuArrayBuffer<C>>,
|
||||||
|
components: Query<(Entity, &C)>,
|
||||||
|
) {
|
||||||
|
gpu_array_buffer.clear();
|
||||||
|
|
||||||
|
let entities = components
|
||||||
|
.iter()
|
||||||
|
.map(|(entity, component)| (entity, gpu_array_buffer.push(component.clone())))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
commands.try_insert_batch(entities);
|
||||||
|
|
||||||
|
gpu_array_buffer.write_buffer(&render_device, &render_queue);
|
||||||
|
}
|
||||||
414
third_party/bevy_render/src/gpu_readback.rs
vendored
Normal file
414
third_party/bevy_render/src/gpu_readback.rs
vendored
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
use crate::{
|
||||||
|
extract_component::ExtractComponentPlugin,
|
||||||
|
render_asset::RenderAssets,
|
||||||
|
render_resource::{
|
||||||
|
Buffer, BufferUsages, CommandEncoder, Extent3d, TexelCopyBufferLayout, Texture,
|
||||||
|
TextureFormat,
|
||||||
|
},
|
||||||
|
renderer::{render_system, RenderDevice},
|
||||||
|
storage::{GpuShaderStorageBuffer, ShaderStorageBuffer},
|
||||||
|
sync_world::MainEntity,
|
||||||
|
texture::GpuImage,
|
||||||
|
ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems,
|
||||||
|
};
|
||||||
|
use async_channel::{Receiver, Sender};
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_asset::Handle;
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::schedule::IntoScheduleConfigs;
|
||||||
|
use bevy_ecs::{
|
||||||
|
change_detection::ResMut,
|
||||||
|
entity::Entity,
|
||||||
|
event::EntityEvent,
|
||||||
|
prelude::{Component, Resource, World},
|
||||||
|
system::{Query, Res},
|
||||||
|
};
|
||||||
|
use bevy_image::{Image, TextureFormatPixelInfo};
|
||||||
|
use bevy_platform::collections::HashMap;
|
||||||
|
use bevy_reflect::Reflect;
|
||||||
|
use bevy_render_macros::ExtractComponent;
|
||||||
|
use encase::internal::ReadFrom;
|
||||||
|
use encase::private::Reader;
|
||||||
|
use encase::ShaderType;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
/// A plugin that enables reading back gpu buffers and textures to the cpu.
|
||||||
|
pub struct GpuReadbackPlugin {
|
||||||
|
/// Describes the number of frames a buffer can be unused before it is removed from the pool in
|
||||||
|
/// order to avoid unnecessary reallocations.
|
||||||
|
max_unused_frames: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GpuReadbackPlugin {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_unused_frames: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for GpuReadbackPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(ExtractComponentPlugin::<Readback>::default());
|
||||||
|
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app
|
||||||
|
.init_resource::<GpuReadbackBufferPool>()
|
||||||
|
.init_resource::<GpuReadbacks>()
|
||||||
|
.insert_resource(GpuReadbackMaxUnusedFrames(self.max_unused_frames))
|
||||||
|
.add_systems(ExtractSchedule, sync_readbacks.ambiguous_with_all())
|
||||||
|
.add_systems(
|
||||||
|
Render,
|
||||||
|
(
|
||||||
|
prepare_buffers.in_set(RenderSystems::PrepareResources),
|
||||||
|
map_buffers
|
||||||
|
.after(render_system)
|
||||||
|
.in_set(RenderSystems::Render),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A component that registers the wrapped handle for gpu readback, either a texture or a buffer.
|
||||||
|
///
|
||||||
|
/// Data is read asynchronously and will be triggered on the entity via the [`ReadbackComplete`] event
|
||||||
|
/// when complete. If this component is not removed, the readback will be attempted every frame
|
||||||
|
#[derive(Component, ExtractComponent, Clone, Debug)]
|
||||||
|
pub enum Readback {
|
||||||
|
Texture(Handle<Image>),
|
||||||
|
Buffer {
|
||||||
|
buffer: Handle<ShaderStorageBuffer>,
|
||||||
|
start_offset_and_size: Option<(u64, u64)>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Readback {
|
||||||
|
/// Create a readback component for a texture using the given handle.
|
||||||
|
pub fn texture(image: Handle<Image>) -> Self {
|
||||||
|
Self::Texture(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a readback component for a full buffer using the given handle.
|
||||||
|
pub fn buffer(buffer: Handle<ShaderStorageBuffer>) -> Self {
|
||||||
|
Self::Buffer {
|
||||||
|
buffer,
|
||||||
|
start_offset_and_size: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a readback component for a buffer range using the given handle, a start offset in bytes
|
||||||
|
/// and a number of bytes to read.
|
||||||
|
pub fn buffer_range(buffer: Handle<ShaderStorageBuffer>, start_offset: u64, size: u64) -> Self {
|
||||||
|
Self::Buffer {
|
||||||
|
buffer,
|
||||||
|
start_offset_and_size: Some((start_offset, size)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event that is triggered when a gpu readback is complete.
|
||||||
|
///
|
||||||
|
/// The event contains the data as a `Vec<u8>`, which can be interpreted as the raw bytes of the
|
||||||
|
/// requested buffer or texture.
|
||||||
|
#[derive(EntityEvent, Deref, DerefMut, Reflect, Debug)]
|
||||||
|
#[reflect(Debug)]
|
||||||
|
pub struct ReadbackComplete {
|
||||||
|
pub entity: Entity,
|
||||||
|
#[deref]
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadbackComplete {
|
||||||
|
/// Convert the raw bytes of the event to a shader type.
|
||||||
|
pub fn to_shader_type<T: ShaderType + ReadFrom + Default>(&self) -> T {
|
||||||
|
let mut val = T::default();
|
||||||
|
let mut reader = Reader::new::<T>(&self.data, 0).expect("Failed to create Reader");
|
||||||
|
T::read_from(&mut val, &mut reader);
|
||||||
|
val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct GpuReadbackMaxUnusedFrames(usize);
|
||||||
|
|
||||||
|
struct GpuReadbackBuffer {
|
||||||
|
buffer: Buffer,
|
||||||
|
taken: bool,
|
||||||
|
frames_unused: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct GpuReadbackBufferPool {
|
||||||
|
// Map of buffer size to list of buffers, with a flag for whether the buffer is taken and how
|
||||||
|
// many frames it has been unused for.
|
||||||
|
// TODO: We could ideally write all readback data to one big buffer per frame, the assumption
|
||||||
|
// here is that very few entities well actually be read back at once, and their size is
|
||||||
|
// unlikely to change.
|
||||||
|
buffers: HashMap<u64, Vec<GpuReadbackBuffer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GpuReadbackBufferPool {
|
||||||
|
fn get(&mut self, render_device: &RenderDevice, size: u64) -> Buffer {
|
||||||
|
let buffers = self.buffers.entry(size).or_default();
|
||||||
|
|
||||||
|
// find an untaken buffer for this size
|
||||||
|
if let Some(buf) = buffers.iter_mut().find(|x| !x.taken) {
|
||||||
|
buf.taken = true;
|
||||||
|
buf.frames_unused = 0;
|
||||||
|
return buf.buffer.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = render_device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("Readback Buffer"),
|
||||||
|
size,
|
||||||
|
usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
buffers.push(GpuReadbackBuffer {
|
||||||
|
buffer: buffer.clone(),
|
||||||
|
taken: true,
|
||||||
|
frames_unused: 0,
|
||||||
|
});
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the buffer to the pool so it can be used in a future frame
|
||||||
|
fn return_buffer(&mut self, buffer: &Buffer) {
|
||||||
|
let size = buffer.size();
|
||||||
|
let buffers = self
|
||||||
|
.buffers
|
||||||
|
.get_mut(&size)
|
||||||
|
.expect("Returned buffer of untracked size");
|
||||||
|
if let Some(buf) = buffers.iter_mut().find(|x| x.buffer.id() == buffer.id()) {
|
||||||
|
buf.taken = false;
|
||||||
|
} else {
|
||||||
|
warn!("Returned buffer that was not allocated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, max_unused_frames: usize) {
|
||||||
|
for (_, buffers) in &mut self.buffers {
|
||||||
|
// Tick all the buffers
|
||||||
|
for buf in &mut *buffers {
|
||||||
|
if !buf.taken {
|
||||||
|
buf.frames_unused += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove buffers that haven't been used for MAX_UNUSED_FRAMES
|
||||||
|
buffers.retain(|x| x.frames_unused < max_unused_frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove empty buffer sizes
|
||||||
|
self.buffers.retain(|_, buffers| !buffers.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReadbackSource {
|
||||||
|
Texture {
|
||||||
|
texture: Texture,
|
||||||
|
layout: TexelCopyBufferLayout,
|
||||||
|
size: Extent3d,
|
||||||
|
},
|
||||||
|
Buffer {
|
||||||
|
buffer: Buffer,
|
||||||
|
start_offset_and_size: Option<(u64, u64)>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct GpuReadbacks {
|
||||||
|
requested: Vec<GpuReadback>,
|
||||||
|
mapped: Vec<GpuReadback>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GpuReadback {
|
||||||
|
pub entity: Entity,
|
||||||
|
pub src: ReadbackSource,
|
||||||
|
pub buffer: Buffer,
|
||||||
|
pub rx: Receiver<(Entity, Buffer, Vec<u8>)>,
|
||||||
|
pub tx: Sender<(Entity, Buffer, Vec<u8>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_readbacks(
|
||||||
|
mut main_world: ResMut<MainWorld>,
|
||||||
|
mut buffer_pool: ResMut<GpuReadbackBufferPool>,
|
||||||
|
mut readbacks: ResMut<GpuReadbacks>,
|
||||||
|
max_unused_frames: Res<GpuReadbackMaxUnusedFrames>,
|
||||||
|
) {
|
||||||
|
readbacks.mapped.retain(|readback| {
|
||||||
|
if let Ok((entity, buffer, data)) = readback.rx.try_recv() {
|
||||||
|
main_world.trigger(ReadbackComplete { data, entity });
|
||||||
|
buffer_pool.return_buffer(&buffer);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buffer_pool.update(max_unused_frames.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_buffers(
|
||||||
|
render_device: Res<RenderDevice>,
|
||||||
|
mut readbacks: ResMut<GpuReadbacks>,
|
||||||
|
mut buffer_pool: ResMut<GpuReadbackBufferPool>,
|
||||||
|
gpu_images: Res<RenderAssets<GpuImage>>,
|
||||||
|
ssbos: Res<RenderAssets<GpuShaderStorageBuffer>>,
|
||||||
|
handles: Query<(&MainEntity, &Readback)>,
|
||||||
|
) {
|
||||||
|
for (entity, readback) in handles.iter() {
|
||||||
|
match readback {
|
||||||
|
Readback::Texture(image) => {
|
||||||
|
if let Some(gpu_image) = gpu_images.get(image)
|
||||||
|
&& let Ok(pixel_size) = gpu_image.texture_format.pixel_size()
|
||||||
|
{
|
||||||
|
let layout = layout_data(gpu_image.size, gpu_image.texture_format);
|
||||||
|
let buffer = buffer_pool.get(
|
||||||
|
&render_device,
|
||||||
|
get_aligned_size(gpu_image.size, pixel_size as u32) as u64,
|
||||||
|
);
|
||||||
|
let (tx, rx) = async_channel::bounded(1);
|
||||||
|
readbacks.requested.push(GpuReadback {
|
||||||
|
entity: entity.id(),
|
||||||
|
src: ReadbackSource::Texture {
|
||||||
|
texture: gpu_image.texture.clone(),
|
||||||
|
layout,
|
||||||
|
size: gpu_image.size,
|
||||||
|
},
|
||||||
|
buffer,
|
||||||
|
rx,
|
||||||
|
tx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Readback::Buffer {
|
||||||
|
buffer,
|
||||||
|
start_offset_and_size,
|
||||||
|
} => {
|
||||||
|
if let Some(ssbo) = ssbos.get(buffer) {
|
||||||
|
let full_size = ssbo.buffer.size();
|
||||||
|
let size = start_offset_and_size
|
||||||
|
.map(|(start, size)| {
|
||||||
|
let end = start + size;
|
||||||
|
if end > full_size {
|
||||||
|
panic!(
|
||||||
|
"Tried to read past the end of the buffer (start: {start}, \
|
||||||
|
size: {size}, buffer size: {full_size})."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
size
|
||||||
|
})
|
||||||
|
.unwrap_or(full_size);
|
||||||
|
let buffer = buffer_pool.get(&render_device, size);
|
||||||
|
let (tx, rx) = async_channel::bounded(1);
|
||||||
|
readbacks.requested.push(GpuReadback {
|
||||||
|
entity: entity.id(),
|
||||||
|
src: ReadbackSource::Buffer {
|
||||||
|
start_offset_and_size: *start_offset_and_size,
|
||||||
|
buffer: ssbo.buffer.clone(),
|
||||||
|
},
|
||||||
|
buffer,
|
||||||
|
rx,
|
||||||
|
tx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn submit_readback_commands(world: &World, command_encoder: &mut CommandEncoder) {
|
||||||
|
let readbacks = world.resource::<GpuReadbacks>();
|
||||||
|
for readback in &readbacks.requested {
|
||||||
|
match &readback.src {
|
||||||
|
ReadbackSource::Texture {
|
||||||
|
texture,
|
||||||
|
layout,
|
||||||
|
size,
|
||||||
|
} => {
|
||||||
|
command_encoder.copy_texture_to_buffer(
|
||||||
|
texture.as_image_copy(),
|
||||||
|
wgpu::TexelCopyBufferInfo {
|
||||||
|
buffer: &readback.buffer,
|
||||||
|
layout: *layout,
|
||||||
|
},
|
||||||
|
*size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ReadbackSource::Buffer {
|
||||||
|
buffer,
|
||||||
|
start_offset_and_size,
|
||||||
|
} => {
|
||||||
|
let (src_start, size) = start_offset_and_size.unwrap_or((0, buffer.size()));
|
||||||
|
command_encoder.copy_buffer_to_buffer(buffer, src_start, &readback.buffer, 0, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move requested readbacks to mapped readbacks after commands have been submitted in render system
|
||||||
|
fn map_buffers(mut readbacks: ResMut<GpuReadbacks>) {
|
||||||
|
let requested = readbacks.requested.drain(..).collect::<Vec<GpuReadback>>();
|
||||||
|
for readback in requested {
|
||||||
|
let slice = readback.buffer.slice(..);
|
||||||
|
let entity = readback.entity;
|
||||||
|
let buffer = readback.buffer.clone();
|
||||||
|
let tx = readback.tx.clone();
|
||||||
|
slice.map_async(wgpu::MapMode::Read, move |res| {
|
||||||
|
res.expect("Failed to map buffer");
|
||||||
|
let buffer_slice = buffer.slice(..);
|
||||||
|
let data = buffer_slice.get_mapped_range();
|
||||||
|
let result = Vec::from(&*data);
|
||||||
|
drop(data);
|
||||||
|
buffer.unmap();
|
||||||
|
if let Err(e) = tx.try_send((entity, buffer, result)) {
|
||||||
|
warn!("Failed to send readback result: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
readbacks.mapped.push(readback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
|
||||||
|
/// Round up a given value to be a multiple of [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`].
|
||||||
|
pub(crate) const fn align_byte_size(value: u32) -> u32 {
|
||||||
|
RenderDevice::align_copy_bytes_per_row(value as usize) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the size of a image when the size of each row has been rounded up to [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`].
|
||||||
|
pub(crate) const fn get_aligned_size(extent: Extent3d, pixel_size: u32) -> u32 {
|
||||||
|
extent.height * align_byte_size(extent.width * pixel_size) * extent.depth_or_array_layers
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a [`TexelCopyBufferLayout`] aligned such that the image can be copied into a buffer.
|
||||||
|
pub(crate) fn layout_data(extent: Extent3d, format: TextureFormat) -> TexelCopyBufferLayout {
|
||||||
|
TexelCopyBufferLayout {
|
||||||
|
bytes_per_row: if extent.height > 1 || extent.depth_or_array_layers > 1 {
|
||||||
|
if let Ok(pixel_size) = format.pixel_size() {
|
||||||
|
// 1 = 1 row
|
||||||
|
Some(get_aligned_size(
|
||||||
|
Extent3d {
|
||||||
|
width: extent.width,
|
||||||
|
height: 1,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
pixel_size as u32,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
rows_per_image: if extent.depth_or_array_layers > 1 {
|
||||||
|
let (_, block_dimension_y) = format.block_dimensions();
|
||||||
|
Some(extent.height / block_dimension_y)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
offset: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
582
third_party/bevy_render/src/lib.rs
vendored
Normal file
582
third_party/bevy_render/src/lib.rs
vendored
Normal file
@ -0,0 +1,582 @@
|
|||||||
|
//! # Useful Environment Variables
|
||||||
|
//!
|
||||||
|
//! Both `bevy_render` and `wgpu` have a number of environment variable options for changing the runtime behavior
|
||||||
|
//! of both crates. Many of these may be useful in development or release environments.
|
||||||
|
//!
|
||||||
|
//! - `WGPU_DEBUG=1` enables debug labels, which can be useful in release builds.
|
||||||
|
//! - `WGPU_VALIDATION=0` disables validation layers. This can help with particularly spammy errors.
|
||||||
|
//! - `WGPU_FORCE_FALLBACK_ADAPTER=1` attempts to force software rendering. This typically matches what is used in CI.
|
||||||
|
//! - `WGPU_ADAPTER_NAME` allows selecting a specific adapter by name.
|
||||||
|
//! - `WGPU_SETTINGS_PRIO=webgl2` uses webgl2 limits.
|
||||||
|
//! - `WGPU_SETTINGS_PRIO=compatibility` uses webgpu limits.
|
||||||
|
//! - `VERBOSE_SHADER_ERROR=1` prints more detailed information about WGSL compilation errors, such as shader defs and shader entrypoint.
|
||||||
|
|
||||||
|
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
|
||||||
|
#![expect(unsafe_code, reason = "Unsafe code is used to improve performance.")]
|
||||||
|
#![cfg_attr(
|
||||||
|
any(docsrs, docsrs_dep),
|
||||||
|
expect(
|
||||||
|
internal_features,
|
||||||
|
reason = "rustdoc_internals is needed for fake_variadic"
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_cfg, rustdoc_internals))]
|
||||||
|
#![doc(
|
||||||
|
html_logo_url = "https://bevy.org/assets/icon.png",
|
||||||
|
html_favicon_url = "https://bevy.org/assets/icon.png"
|
||||||
|
)]
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "16")]
|
||||||
|
compile_error!("bevy_render cannot compile for a 16-bit platform.");
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
extern crate core;
|
||||||
|
|
||||||
|
// Required to make proc macros work in bevy itself.
|
||||||
|
extern crate self as bevy_render;
|
||||||
|
|
||||||
|
pub mod alpha;
|
||||||
|
pub mod batching;
|
||||||
|
pub mod camera;
|
||||||
|
pub mod diagnostic;
|
||||||
|
pub mod erased_render_asset;
|
||||||
|
pub mod experimental;
|
||||||
|
pub mod extract_component;
|
||||||
|
pub mod extract_instances;
|
||||||
|
mod extract_param;
|
||||||
|
pub mod extract_resource;
|
||||||
|
pub mod globals;
|
||||||
|
pub mod gpu_component_array_buffer;
|
||||||
|
pub mod gpu_readback;
|
||||||
|
pub mod mesh;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub mod pipelined_rendering;
|
||||||
|
pub mod render_asset;
|
||||||
|
pub mod render_graph;
|
||||||
|
pub mod render_phase;
|
||||||
|
pub mod render_resource;
|
||||||
|
pub mod renderer;
|
||||||
|
pub mod settings;
|
||||||
|
pub mod storage;
|
||||||
|
pub mod sync_component;
|
||||||
|
pub mod sync_world;
|
||||||
|
pub mod texture;
|
||||||
|
pub mod view;
|
||||||
|
|
||||||
|
/// The render prelude.
|
||||||
|
///
|
||||||
|
/// This includes the most common types in this crate, re-exported for your convenience.
|
||||||
|
pub mod prelude {
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use crate::{
|
||||||
|
alpha::AlphaMode, camera::NormalizedRenderTargetExt as _, texture::ManualTextureViews,
|
||||||
|
view::Msaa, ExtractSchedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use extract_param::Extract;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
camera::CameraPlugin,
|
||||||
|
gpu_readback::GpuReadbackPlugin,
|
||||||
|
mesh::{MeshRenderAssetPlugin, RenderMesh},
|
||||||
|
render_asset::prepare_assets,
|
||||||
|
render_resource::PipelineCache,
|
||||||
|
renderer::{render_system, RenderAdapterInfo},
|
||||||
|
settings::RenderCreation,
|
||||||
|
storage::StoragePlugin,
|
||||||
|
texture::TexturePlugin,
|
||||||
|
view::{ViewPlugin, WindowRenderPlugin},
|
||||||
|
};
|
||||||
|
use alloc::sync::Arc;
|
||||||
|
use batching::gpu_preprocessing::BatchingPlugin;
|
||||||
|
use bevy_app::{App, AppLabel, Plugin, SubApp};
|
||||||
|
use bevy_asset::{AssetApp, AssetServer};
|
||||||
|
use bevy_ecs::{
|
||||||
|
prelude::*,
|
||||||
|
schedule::{ScheduleBuildSettings, ScheduleLabel},
|
||||||
|
};
|
||||||
|
use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats};
|
||||||
|
use bevy_shader::{load_shader_library, Shader, ShaderLoader};
|
||||||
|
use bevy_utils::prelude::default;
|
||||||
|
use bevy_window::{PrimaryWindow, RawHandleWrapperHolder};
|
||||||
|
use bitflags::bitflags;
|
||||||
|
use core::ops::{Deref, DerefMut};
|
||||||
|
use experimental::occlusion_culling::OcclusionCullingPlugin;
|
||||||
|
use globals::GlobalsPlugin;
|
||||||
|
use render_asset::{
|
||||||
|
extract_render_asset_bytes_per_frame, reset_render_asset_bytes_per_frame,
|
||||||
|
RenderAssetBytesPerFrame, RenderAssetBytesPerFrameLimiter,
|
||||||
|
};
|
||||||
|
use settings::RenderResources;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use sync_world::{despawn_temporary_render_entities, entity_sync_system, SyncWorldPlugin};
|
||||||
|
|
||||||
|
/// Contains the default Bevy rendering backend based on wgpu.
|
||||||
|
///
|
||||||
|
/// Rendering is done in a [`SubApp`], which exchanges data with the main app
|
||||||
|
/// between main schedule iterations.
|
||||||
|
///
|
||||||
|
/// Rendering can be executed between iterations of the main schedule,
|
||||||
|
/// or it can be executed in parallel with main schedule when
|
||||||
|
/// [`PipelinedRenderingPlugin`](pipelined_rendering::PipelinedRenderingPlugin) is enabled.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct RenderPlugin {
|
||||||
|
pub render_creation: RenderCreation,
|
||||||
|
/// If `true`, disables asynchronous pipeline compilation.
|
||||||
|
/// This has no effect on macOS, Wasm, iOS, or without the `multi_threaded` feature.
|
||||||
|
pub synchronous_pipeline_compilation: bool,
|
||||||
|
/// Debugging flags that can optionally be set when constructing the renderer.
|
||||||
|
pub debug_flags: RenderDebugFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
/// Debugging flags that can optionally be set when constructing the renderer.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Default, Debug)]
|
||||||
|
pub struct RenderDebugFlags: u8 {
|
||||||
|
/// If true, this sets the `COPY_SRC` flag on indirect draw parameters
|
||||||
|
/// so that they can be read back to CPU.
|
||||||
|
///
|
||||||
|
/// This is a debugging feature that may reduce performance. It
|
||||||
|
/// primarily exists for the `occlusion_culling` example.
|
||||||
|
const ALLOW_COPIES_FROM_INDIRECT_PARAMETERS = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The systems sets of the default [`App`] rendering schedule.
|
||||||
|
///
|
||||||
|
/// These can be useful for ordering, but you almost never want to add your systems to these sets.
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||||
|
pub enum RenderSystems {
|
||||||
|
/// This is used for applying the commands from the [`ExtractSchedule`]
|
||||||
|
ExtractCommands,
|
||||||
|
/// Prepare assets that have been created/modified/removed this frame.
|
||||||
|
PrepareAssets,
|
||||||
|
/// Prepares extracted meshes.
|
||||||
|
PrepareMeshes,
|
||||||
|
/// Create any additional views such as those used for shadow mapping.
|
||||||
|
ManageViews,
|
||||||
|
/// Queue drawable entities as phase items in render phases ready for
|
||||||
|
/// sorting (if necessary)
|
||||||
|
Queue,
|
||||||
|
/// A sub-set within [`Queue`](RenderSystems::Queue) where mesh entity queue systems are executed. Ensures `prepare_assets::<RenderMesh>` is completed.
|
||||||
|
QueueMeshes,
|
||||||
|
/// A sub-set within [`Queue`](RenderSystems::Queue) where meshes that have
|
||||||
|
/// become invisible or changed phases are removed from the bins.
|
||||||
|
QueueSweep,
|
||||||
|
// TODO: This could probably be moved in favor of a system ordering
|
||||||
|
// abstraction in `Render` or `Queue`
|
||||||
|
/// Sort the [`SortedRenderPhase`](render_phase::SortedRenderPhase)s and
|
||||||
|
/// [`BinKey`](render_phase::BinnedPhaseItem::BinKey)s here.
|
||||||
|
PhaseSort,
|
||||||
|
/// Prepare render resources from extracted data for the GPU based on their sorted order.
|
||||||
|
/// Create [`BindGroups`](render_resource::BindGroup) that depend on those data.
|
||||||
|
Prepare,
|
||||||
|
/// A sub-set within [`Prepare`](RenderSystems::Prepare) for initializing buffers, textures and uniforms for use in bind groups.
|
||||||
|
PrepareResources,
|
||||||
|
/// Collect phase buffers after
|
||||||
|
/// [`PrepareResources`](RenderSystems::PrepareResources) has run.
|
||||||
|
PrepareResourcesCollectPhaseBuffers,
|
||||||
|
/// Flush buffers after [`PrepareResources`](RenderSystems::PrepareResources), but before [`PrepareBindGroups`](RenderSystems::PrepareBindGroups).
|
||||||
|
PrepareResourcesFlush,
|
||||||
|
/// A sub-set within [`Prepare`](RenderSystems::Prepare) for constructing bind groups, or other data that relies on render resources prepared in [`PrepareResources`](RenderSystems::PrepareResources).
|
||||||
|
PrepareBindGroups,
|
||||||
|
/// Actual rendering happens here.
|
||||||
|
/// In most cases, only the render backend should insert resources here.
|
||||||
|
Render,
|
||||||
|
/// Cleanup render resources here.
|
||||||
|
Cleanup,
|
||||||
|
/// Final cleanup occurs: any entities with
|
||||||
|
/// [`TemporaryRenderEntity`](sync_world::TemporaryRenderEntity) will be despawned.
|
||||||
|
///
|
||||||
|
/// Runs after [`Cleanup`](RenderSystems::Cleanup).
|
||||||
|
PostCleanup,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The startup schedule of the [`RenderApp`]
|
||||||
|
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)]
|
||||||
|
pub struct RenderStartup;
|
||||||
|
|
||||||
|
/// The main render schedule.
|
||||||
|
#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone, Default)]
|
||||||
|
pub struct Render;
|
||||||
|
|
||||||
|
impl Render {
|
||||||
|
/// Sets up the base structure of the rendering [`Schedule`].
|
||||||
|
///
|
||||||
|
/// The sets defined in this enum are configured to run in order.
|
||||||
|
pub fn base_schedule() -> Schedule {
|
||||||
|
use RenderSystems::*;
|
||||||
|
|
||||||
|
let mut schedule = Schedule::new(Self);
|
||||||
|
|
||||||
|
schedule.configure_sets(
|
||||||
|
(
|
||||||
|
ExtractCommands,
|
||||||
|
PrepareMeshes,
|
||||||
|
ManageViews,
|
||||||
|
Queue,
|
||||||
|
PhaseSort,
|
||||||
|
Prepare,
|
||||||
|
Render,
|
||||||
|
Cleanup,
|
||||||
|
PostCleanup,
|
||||||
|
)
|
||||||
|
.chain(),
|
||||||
|
);
|
||||||
|
|
||||||
|
schedule.configure_sets((ExtractCommands, PrepareAssets, PrepareMeshes, Prepare).chain());
|
||||||
|
schedule.configure_sets(
|
||||||
|
(QueueMeshes, QueueSweep)
|
||||||
|
.chain()
|
||||||
|
.in_set(Queue)
|
||||||
|
.after(prepare_assets::<RenderMesh>),
|
||||||
|
);
|
||||||
|
schedule.configure_sets(
|
||||||
|
(
|
||||||
|
PrepareResources,
|
||||||
|
PrepareResourcesCollectPhaseBuffers,
|
||||||
|
PrepareResourcesFlush,
|
||||||
|
PrepareBindGroups,
|
||||||
|
)
|
||||||
|
.chain()
|
||||||
|
.in_set(Prepare),
|
||||||
|
);
|
||||||
|
|
||||||
|
schedule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schedule in which data from the main world is 'extracted' into the render world.
|
||||||
|
///
|
||||||
|
/// This step should be kept as short as possible to increase the "pipelining potential" for
|
||||||
|
/// running the next frame while rendering the current frame.
|
||||||
|
///
|
||||||
|
/// This schedule is run on the render world, but it also has access to the main world.
|
||||||
|
/// See [`MainWorld`] and [`Extract`] for details on how to access main world data from this schedule.
|
||||||
|
#[derive(ScheduleLabel, PartialEq, Eq, Debug, Clone, Hash, Default)]
|
||||||
|
pub struct ExtractSchedule;
|
||||||
|
|
||||||
|
/// The simulation [`World`] of the application, stored as a resource.
|
||||||
|
///
|
||||||
|
/// This resource is only available during [`ExtractSchedule`] and not
|
||||||
|
/// during command application of that schedule.
|
||||||
|
/// See [`Extract`] for more details.
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct MainWorld(World);
|
||||||
|
|
||||||
|
impl Deref for MainWorld {
|
||||||
|
type Target = World;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for MainWorld {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod graph {
|
||||||
|
use crate::render_graph::RenderLabel;
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
|
||||||
|
pub struct CameraDriverLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct FutureRenderResources(Arc<Mutex<Option<RenderResources>>>);
|
||||||
|
|
||||||
|
/// A label for the rendering sub-app.
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]
|
||||||
|
pub struct RenderApp;
|
||||||
|
|
||||||
|
impl Plugin for RenderPlugin {
|
||||||
|
/// Initializes the renderer, sets up the [`RenderSystems`] and creates the rendering sub-app.
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_asset::<Shader>()
|
||||||
|
.init_asset_loader::<ShaderLoader>();
|
||||||
|
|
||||||
|
match &self.render_creation {
|
||||||
|
RenderCreation::Manual(resources) => {
|
||||||
|
let future_render_resources_wrapper = Arc::new(Mutex::new(Some(resources.clone())));
|
||||||
|
app.insert_resource(FutureRenderResources(
|
||||||
|
future_render_resources_wrapper.clone(),
|
||||||
|
));
|
||||||
|
// SAFETY: Plugins should be set up on the main thread.
|
||||||
|
unsafe { initialize_render_app(app) };
|
||||||
|
}
|
||||||
|
RenderCreation::Automatic(render_creation) => {
|
||||||
|
if let Some(backends) = render_creation.backends {
|
||||||
|
let future_render_resources_wrapper = Arc::new(Mutex::new(None));
|
||||||
|
app.insert_resource(FutureRenderResources(
|
||||||
|
future_render_resources_wrapper.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let primary_window = app
|
||||||
|
.world_mut()
|
||||||
|
.query_filtered::<&RawHandleWrapperHolder, With<PrimaryWindow>>()
|
||||||
|
.single(app.world())
|
||||||
|
.ok()
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let settings = render_creation.clone();
|
||||||
|
|
||||||
|
#[cfg(feature = "raw_vulkan_init")]
|
||||||
|
let raw_vulkan_init_settings = app
|
||||||
|
.world_mut()
|
||||||
|
.get_resource::<renderer::raw_vulkan_init::RawVulkanInitSettings>()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let async_renderer = async move {
|
||||||
|
let render_resources = renderer::initialize_renderer(
|
||||||
|
backends,
|
||||||
|
primary_window,
|
||||||
|
&settings,
|
||||||
|
#[cfg(feature = "raw_vulkan_init")]
|
||||||
|
raw_vulkan_init_settings,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
*future_render_resources_wrapper.lock().unwrap() = Some(render_resources);
|
||||||
|
};
|
||||||
|
|
||||||
|
// In wasm, spawn a task and detach it for execution
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
bevy_tasks::IoTaskPool::get()
|
||||||
|
.spawn_local(async_renderer)
|
||||||
|
.detach();
|
||||||
|
// Otherwise, just block for it to complete
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
bevy_tasks::block_on(async_renderer);
|
||||||
|
|
||||||
|
// SAFETY: Plugins should be set up on the main thread.
|
||||||
|
unsafe { initialize_render_app(app) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.add_plugins((
|
||||||
|
WindowRenderPlugin,
|
||||||
|
CameraPlugin,
|
||||||
|
ViewPlugin,
|
||||||
|
MeshRenderAssetPlugin,
|
||||||
|
GlobalsPlugin,
|
||||||
|
#[cfg(feature = "morph")]
|
||||||
|
mesh::MorphPlugin,
|
||||||
|
TexturePlugin,
|
||||||
|
BatchingPlugin {
|
||||||
|
debug_flags: self.debug_flags,
|
||||||
|
},
|
||||||
|
SyncWorldPlugin,
|
||||||
|
StoragePlugin,
|
||||||
|
GpuReadbackPlugin::default(),
|
||||||
|
OcclusionCullingPlugin,
|
||||||
|
#[cfg(feature = "tracing-tracy")]
|
||||||
|
diagnostic::RenderDiagnosticsPlugin,
|
||||||
|
));
|
||||||
|
|
||||||
|
app.init_resource::<RenderAssetBytesPerFrame>();
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app.init_resource::<RenderAssetBytesPerFrameLimiter>();
|
||||||
|
render_app
|
||||||
|
.add_systems(ExtractSchedule, extract_render_asset_bytes_per_frame)
|
||||||
|
.add_systems(
|
||||||
|
Render,
|
||||||
|
reset_render_asset_bytes_per_frame.in_set(RenderSystems::Cleanup),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ready(&self, app: &App) -> bool {
|
||||||
|
app.world()
|
||||||
|
.get_resource::<FutureRenderResources>()
|
||||||
|
.and_then(|frr| frr.0.try_lock().map(|locked| locked.is_some()).ok())
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self, app: &mut App) {
|
||||||
|
load_shader_library!(app, "maths.wgsl");
|
||||||
|
load_shader_library!(app, "color_operations.wgsl");
|
||||||
|
load_shader_library!(app, "bindless.wgsl");
|
||||||
|
if let Some(future_render_resources) =
|
||||||
|
app.world_mut().remove_resource::<FutureRenderResources>()
|
||||||
|
{
|
||||||
|
let render_resources = future_render_resources.0.lock().unwrap().take().unwrap();
|
||||||
|
let RenderResources(device, queue, adapter_info, render_adapter, instance, ..) =
|
||||||
|
render_resources;
|
||||||
|
|
||||||
|
let compressed_image_format_support = CompressedImageFormatSupport(
|
||||||
|
CompressedImageFormats::from_features(device.features()),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.insert_resource(device.clone())
|
||||||
|
.insert_resource(queue.clone())
|
||||||
|
.insert_resource(adapter_info.clone())
|
||||||
|
.insert_resource(render_adapter.clone())
|
||||||
|
.insert_resource(compressed_image_format_support);
|
||||||
|
|
||||||
|
let render_app = app.sub_app_mut(RenderApp);
|
||||||
|
|
||||||
|
#[cfg(feature = "raw_vulkan_init")]
|
||||||
|
{
|
||||||
|
let additional_vulkan_features: renderer::raw_vulkan_init::AdditionalVulkanFeatures =
|
||||||
|
render_resources.5;
|
||||||
|
render_app.insert_resource(additional_vulkan_features);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_app
|
||||||
|
.insert_resource(instance)
|
||||||
|
.insert_resource(PipelineCache::new(
|
||||||
|
device.clone(),
|
||||||
|
render_adapter.clone(),
|
||||||
|
self.synchronous_pipeline_compilation,
|
||||||
|
))
|
||||||
|
.insert_resource(device)
|
||||||
|
.insert_resource(queue)
|
||||||
|
.insert_resource(render_adapter)
|
||||||
|
.insert_resource(adapter_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A "scratch" world used to avoid allocating new worlds every frame when
|
||||||
|
/// swapping out the [`MainWorld`] for [`ExtractSchedule`].
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct ScratchMainWorld(World);
|
||||||
|
|
||||||
|
/// Executes the [`ExtractSchedule`] step of the renderer.
|
||||||
|
/// This updates the render world with the extracted ECS data of the current frame.
|
||||||
|
fn extract(main_world: &mut World, render_world: &mut World) {
|
||||||
|
// temporarily add the app world to the render world as a resource
|
||||||
|
let scratch_world = main_world.remove_resource::<ScratchMainWorld>().unwrap();
|
||||||
|
let inserted_world = core::mem::replace(main_world, scratch_world.0);
|
||||||
|
render_world.insert_resource(MainWorld(inserted_world));
|
||||||
|
render_world.run_schedule(ExtractSchedule);
|
||||||
|
|
||||||
|
// move the app world back, as if nothing happened.
|
||||||
|
let inserted_world = render_world.remove_resource::<MainWorld>().unwrap();
|
||||||
|
let scratch_world = core::mem::replace(main_world, inserted_world.0);
|
||||||
|
main_world.insert_resource(ScratchMainWorld(scratch_world));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// This function must be called from the main thread.
|
||||||
|
unsafe fn initialize_render_app(app: &mut App) {
|
||||||
|
app.init_resource::<ScratchMainWorld>();
|
||||||
|
|
||||||
|
let mut render_app = SubApp::new();
|
||||||
|
render_app.update_schedule = Some(Render.intern());
|
||||||
|
|
||||||
|
let mut extract_schedule = Schedule::new(ExtractSchedule);
|
||||||
|
// We skip applying any commands during the ExtractSchedule
|
||||||
|
// so commands can be applied on the render thread.
|
||||||
|
extract_schedule.set_build_settings(ScheduleBuildSettings {
|
||||||
|
auto_insert_apply_deferred: false,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
extract_schedule.set_apply_final_deferred(false);
|
||||||
|
|
||||||
|
render_app
|
||||||
|
.add_schedule(extract_schedule)
|
||||||
|
.add_schedule(Render::base_schedule())
|
||||||
|
.init_resource::<render_graph::RenderGraph>()
|
||||||
|
.insert_resource(app.world().resource::<AssetServer>().clone())
|
||||||
|
.add_systems(ExtractSchedule, PipelineCache::extract_shaders)
|
||||||
|
.add_systems(
|
||||||
|
Render,
|
||||||
|
(
|
||||||
|
// This set applies the commands from the extract schedule while the render schedule
|
||||||
|
// is running in parallel with the main app.
|
||||||
|
apply_extract_commands.in_set(RenderSystems::ExtractCommands),
|
||||||
|
(PipelineCache::process_pipeline_queue_system, render_system)
|
||||||
|
.chain()
|
||||||
|
.in_set(RenderSystems::Render),
|
||||||
|
despawn_temporary_render_entities.in_set(RenderSystems::PostCleanup),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// We want the closure to have a flag to only run the RenderStartup schedule once, but the only
|
||||||
|
// way to have the closure store this flag is by capturing it. This variable is otherwise
|
||||||
|
// unused.
|
||||||
|
let mut should_run_startup = true;
|
||||||
|
render_app.set_extract(move |main_world, render_world| {
|
||||||
|
if should_run_startup {
|
||||||
|
// Run the `RenderStartup` if it hasn't run yet. This does mean `RenderStartup` blocks
|
||||||
|
// the rest of the app extraction, but this is necessary since extraction itself can
|
||||||
|
// depend on resources initialized in `RenderStartup`.
|
||||||
|
render_world.run_schedule(RenderStartup);
|
||||||
|
should_run_startup = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
#[cfg(feature = "trace")]
|
||||||
|
let _stage_span = tracing::info_span!("entity_sync").entered();
|
||||||
|
entity_sync_system(main_world, render_world);
|
||||||
|
}
|
||||||
|
|
||||||
|
// run extract schedule
|
||||||
|
extract(main_world, render_world);
|
||||||
|
});
|
||||||
|
|
||||||
|
let (sender, receiver) = bevy_time::create_time_channels();
|
||||||
|
render_app.insert_resource(sender);
|
||||||
|
app.insert_resource(receiver);
|
||||||
|
app.insert_sub_app(RenderApp, render_app);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies the commands from the extract schedule. This happens during
|
||||||
|
/// the render schedule rather than during extraction to allow the commands to run in parallel with the
|
||||||
|
/// main app when pipelined rendering is enabled.
|
||||||
|
fn apply_extract_commands(render_world: &mut World) {
|
||||||
|
render_world.resource_scope(|render_world, mut schedules: Mut<Schedules>| {
|
||||||
|
schedules
|
||||||
|
.get_mut(ExtractSchedule)
|
||||||
|
.unwrap()
|
||||||
|
.apply_deferred(render_world);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the [`RenderAdapterInfo`] is a Qualcomm Adreno, returns its model number.
|
||||||
|
///
|
||||||
|
/// This lets us work around hardware bugs.
|
||||||
|
pub fn get_adreno_model(adapter_info: &RenderAdapterInfo) -> Option<u32> {
|
||||||
|
if !cfg!(target_os = "android") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let adreno_model = adapter_info.name.strip_prefix("Adreno (TM) ")?;
|
||||||
|
|
||||||
|
// Take suffixes into account (like Adreno 642L).
|
||||||
|
Some(
|
||||||
|
adreno_model
|
||||||
|
.chars()
|
||||||
|
.map_while(|c| c.to_digit(10))
|
||||||
|
.fold(0, |acc, digit| acc * 10 + digit),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the Mali driver version if the adapter is a Mali GPU.
|
||||||
|
pub fn get_mali_driver_version(adapter_info: &RenderAdapterInfo) -> Option<u32> {
|
||||||
|
if !cfg!(target_os = "android") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !adapter_info.name.contains("Mali") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let driver_info = &adapter_info.driver_info;
|
||||||
|
if let Some(start_pos) = driver_info.find("v1.r")
|
||||||
|
&& let Some(end_pos) = driver_info[start_pos..].find('p')
|
||||||
|
{
|
||||||
|
let start_idx = start_pos + 4; // Skip "v1.r"
|
||||||
|
let end_idx = start_pos + end_pos;
|
||||||
|
|
||||||
|
return driver_info[start_idx..end_idx].parse::<u32>().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
186
third_party/bevy_render/src/maths.wgsl
vendored
Normal file
186
third_party/bevy_render/src/maths.wgsl
vendored
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
#define_import_path bevy_render::maths
|
||||||
|
|
||||||
|
const PI: f32 = 3.141592653589793; // π
|
||||||
|
const PI_2: f32 = 6.283185307179586; // 2π
|
||||||
|
const HALF_PI: f32 = 1.57079632679; // π/2
|
||||||
|
const FRAC_PI_3: f32 = 1.0471975512; // π/3
|
||||||
|
const E: f32 = 2.718281828459045; // exp(1)
|
||||||
|
|
||||||
|
fn affine2_to_square(affine: mat3x2<f32>) -> mat3x3<f32> {
|
||||||
|
return mat3x3<f32>(
|
||||||
|
vec3<f32>(affine[0].xy, 0.0),
|
||||||
|
vec3<f32>(affine[1].xy, 0.0),
|
||||||
|
vec3<f32>(affine[2].xy, 1.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn affine3_to_square(affine: mat3x4<f32>) -> mat4x4<f32> {
|
||||||
|
return transpose(mat4x4<f32>(
|
||||||
|
affine[0],
|
||||||
|
affine[1],
|
||||||
|
affine[2],
|
||||||
|
vec4<f32>(0.0, 0.0, 0.0, 1.0),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mat2x4_f32_to_mat3x3_unpack(
|
||||||
|
a: mat2x4<f32>,
|
||||||
|
b: f32,
|
||||||
|
) -> mat3x3<f32> {
|
||||||
|
return mat3x3<f32>(
|
||||||
|
a[0].xyz,
|
||||||
|
vec3<f32>(a[0].w, a[1].xy),
|
||||||
|
vec3<f32>(a[1].zw, b),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts the square portion of an affine matrix: i.e. discards the
|
||||||
|
// translation.
|
||||||
|
fn affine3_to_mat3x3(affine: mat4x3<f32>) -> mat3x3<f32> {
|
||||||
|
return mat3x3<f32>(affine[0].xyz, affine[1].xyz, affine[2].xyz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the inverse of a 3x3 matrix.
|
||||||
|
fn inverse_mat3x3(matrix: mat3x3<f32>) -> mat3x3<f32> {
|
||||||
|
let tmp0 = cross(matrix[1], matrix[2]);
|
||||||
|
let tmp1 = cross(matrix[2], matrix[0]);
|
||||||
|
let tmp2 = cross(matrix[0], matrix[1]);
|
||||||
|
let inv_det = 1.0 / dot(matrix[2], tmp2);
|
||||||
|
return transpose(mat3x3<f32>(tmp0 * inv_det, tmp1 * inv_det, tmp2 * inv_det));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the inverse of an affine matrix.
|
||||||
|
//
|
||||||
|
// https://en.wikipedia.org/wiki/Affine_transformation#Groups
|
||||||
|
fn inverse_affine3(affine: mat4x3<f32>) -> mat4x3<f32> {
|
||||||
|
let matrix3 = affine3_to_mat3x3(affine);
|
||||||
|
let inv_matrix3 = inverse_mat3x3(matrix3);
|
||||||
|
return mat4x3<f32>(inv_matrix3[0], inv_matrix3[1], inv_matrix3[2], -(inv_matrix3 * affine[3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts the upper 3x3 portion of a 4x4 matrix.
|
||||||
|
fn mat4x4_to_mat3x3(m: mat4x4<f32>) -> mat3x3<f32> {
|
||||||
|
return mat3x3<f32>(m[0].xyz, m[1].xyz, m[2].xyz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the sign bit from B onto A.
|
||||||
|
// copysign allows proper handling of negative zero to match the rust implementation of orthonormalize
|
||||||
|
fn copysign(a: f32, b: f32) -> f32 {
|
||||||
|
return bitcast<f32>((bitcast<u32>(a) & 0x7FFFFFFF) | (bitcast<u32>(b) & 0x80000000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructs a right-handed orthonormal basis from a given unit Z vector.
|
||||||
|
//
|
||||||
|
// NOTE: requires unit-length (normalized) input to function properly.
|
||||||
|
//
|
||||||
|
// https://jcgt.org/published/0006/01/01/paper.pdf
|
||||||
|
// this method of constructing a basis from a vec3 is also used by `glam::Vec3::any_orthonormal_pair`
|
||||||
|
// the construction of the orthonormal basis up and right vectors here needs to precisely match the rust
|
||||||
|
// implementation in bevy_light/spot_light.rs:spot_light_world_from_view
|
||||||
|
fn orthonormalize(z_basis: vec3<f32>) -> mat3x3<f32> {
|
||||||
|
let sign = copysign(1.0, z_basis.z);
|
||||||
|
let a = -1.0 / (sign + z_basis.z);
|
||||||
|
let b = z_basis.x * z_basis.y * a;
|
||||||
|
let x_basis = vec3(1.0 + sign * z_basis.x * z_basis.x * a, sign * b, -sign * z_basis.x);
|
||||||
|
let y_basis = vec3(b, sign + z_basis.y * z_basis.y * a, -z_basis.y);
|
||||||
|
return mat3x3(x_basis, y_basis, z_basis);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if any part of a sphere is on the positive side of a plane.
|
||||||
|
//
|
||||||
|
// `sphere_center.w` should be 1.0.
|
||||||
|
//
|
||||||
|
// This is used for frustum culling.
|
||||||
|
fn sphere_intersects_plane_half_space(
|
||||||
|
plane: vec4<f32>,
|
||||||
|
sphere_center: vec4<f32>,
|
||||||
|
sphere_radius: f32
|
||||||
|
) -> bool {
|
||||||
|
return dot(plane, sphere_center) + sphere_radius > 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the distances along the ray to its intersections with a sphere
|
||||||
|
// centered at the origin.
|
||||||
|
//
|
||||||
|
// r: distance from the sphere center to the ray origin
|
||||||
|
// mu: cosine of the zenith angle
|
||||||
|
// sphere_radius: radius of the sphere
|
||||||
|
//
|
||||||
|
// Returns vec2(t0, t1). If there is no intersection, returns vec2(-1.0).
|
||||||
|
fn ray_sphere_intersect(r: f32, mu: f32, sphere_radius: f32) -> vec2<f32> {
|
||||||
|
let discriminant = r * r * (mu * mu - 1.0) + sphere_radius * sphere_radius;
|
||||||
|
|
||||||
|
// No intersection
|
||||||
|
if discriminant < 0.0 {
|
||||||
|
return vec2(-1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let q = -r * mu;
|
||||||
|
let sqrt_discriminant = sqrt(discriminant);
|
||||||
|
|
||||||
|
// Return both intersection distances
|
||||||
|
return vec2(
|
||||||
|
q - sqrt_discriminant,
|
||||||
|
q + sqrt_discriminant
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pow() but safe for NaNs/negatives
|
||||||
|
fn powsafe(color: vec3<f32>, power: f32) -> vec3<f32> {
|
||||||
|
return pow(abs(color), vec3(power)) * sign(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/Vector_projection#Vector_projection_2
|
||||||
|
fn project_onto(lhs: vec3<f32>, rhs: vec3<f32>) -> vec3<f32> {
|
||||||
|
let other_len_sq_rcp = 1.0 / dot(rhs, rhs);
|
||||||
|
return rhs * dot(lhs, rhs) * other_len_sq_rcp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below are fast approximations of common irrational and trig functions. These
|
||||||
|
// are likely most useful when raymarching, for example, where complete numeric
|
||||||
|
// accuracy can be sacrificed for greater sample count.
|
||||||
|
|
||||||
|
// Slightly less accurate than fast_acos_4, but much simpler.
|
||||||
|
fn fast_acos(in_x: f32) -> f32 {
|
||||||
|
let x = abs(in_x);
|
||||||
|
var res = -0.156583 * x + HALF_PI;
|
||||||
|
res *= sqrt(1.0 - x);
|
||||||
|
return select(PI - res, res, in_x >= 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4th order polynomial approximation
|
||||||
|
// 4 VGRP, 16 ALU Full Rate
|
||||||
|
// 7 * 10^-5 radians precision
|
||||||
|
// Reference : Handbook of Mathematical Functions (chapter : Elementary Transcendental Functions), M. Abramowitz and I.A. Stegun, Ed.
|
||||||
|
fn fast_acos_4(x: f32) -> f32 {
|
||||||
|
let x1 = abs(x);
|
||||||
|
let x2 = x1 * x1;
|
||||||
|
let x3 = x2 * x1;
|
||||||
|
var s: f32;
|
||||||
|
|
||||||
|
s = -0.2121144 * x1 + 1.5707288;
|
||||||
|
s = 0.0742610 * x2 + s;
|
||||||
|
s = -0.0187293 * x3 + s;
|
||||||
|
s = sqrt(1.0 - x1) * s;
|
||||||
|
|
||||||
|
// acos function mirroring
|
||||||
|
return select(PI - s, s, x >= 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fast_atan2(y: f32, x: f32) -> f32 {
|
||||||
|
var t0 = max(abs(x), abs(y));
|
||||||
|
var t1 = min(abs(x), abs(y));
|
||||||
|
var t3 = t1 / t0;
|
||||||
|
var t4 = t3 * t3;
|
||||||
|
|
||||||
|
t0 = 0.0872929;
|
||||||
|
t0 = t0 * t4 - 0.301895;
|
||||||
|
t0 = t0 * t4 + 1.0;
|
||||||
|
t3 = t0 * t3;
|
||||||
|
|
||||||
|
t3 = select(t3, (0.5 * PI) - t3, abs(y) > abs(x));
|
||||||
|
t3 = select(t3, PI - t3, x < 0);
|
||||||
|
t3 = select(-t3, t3, y > 0);
|
||||||
|
|
||||||
|
return t3;
|
||||||
|
}
|
||||||
1056
third_party/bevy_render/src/mesh/allocator.rs
vendored
Normal file
1056
third_party/bevy_render/src/mesh/allocator.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
203
third_party/bevy_render/src/mesh/mod.rs
vendored
Normal file
203
third_party/bevy_render/src/mesh/mod.rs
vendored
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
pub mod allocator;
|
||||||
|
use crate::{
|
||||||
|
render_asset::{
|
||||||
|
AssetExtractionError, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets,
|
||||||
|
},
|
||||||
|
texture::GpuImage,
|
||||||
|
RenderApp,
|
||||||
|
};
|
||||||
|
use allocator::MeshAllocatorPlugin;
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_asset::{AssetId, RenderAssetUsages};
|
||||||
|
use bevy_ecs::{
|
||||||
|
prelude::*,
|
||||||
|
system::{
|
||||||
|
lifetimeless::{SRes, SResMut},
|
||||||
|
SystemParamItem,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
#[cfg(feature = "morph")]
|
||||||
|
use bevy_mesh::morph::{MeshMorphWeights, MorphWeights};
|
||||||
|
use bevy_mesh::*;
|
||||||
|
use wgpu::IndexFormat;
|
||||||
|
|
||||||
|
/// Makes sure that [`Mesh`]es are extracted and prepared for the GPU.
|
||||||
|
/// Does *not* add the [`Mesh`] as an asset. Use [`MeshPlugin`] for that.
|
||||||
|
pub struct MeshRenderAssetPlugin;
|
||||||
|
|
||||||
|
impl Plugin for MeshRenderAssetPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app
|
||||||
|
// 'Mesh' must be prepared after 'Image' as meshes rely on the morph target image being ready
|
||||||
|
.add_plugins(RenderAssetPlugin::<RenderMesh, GpuImage>::default())
|
||||||
|
.add_plugins(MeshAllocatorPlugin);
|
||||||
|
|
||||||
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
render_app.init_resource::<MeshVertexBufferLayouts>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [Inherit weights](inherit_weights) from glTF mesh parent entity to direct
|
||||||
|
/// bevy mesh child entities (ie: glTF primitive).
|
||||||
|
#[cfg(feature = "morph")]
|
||||||
|
pub struct MorphPlugin;
|
||||||
|
#[cfg(feature = "morph")]
|
||||||
|
impl Plugin for MorphPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
bevy_app::PostUpdate,
|
||||||
|
inherit_weights.in_set(InheritWeightSystems),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bevy meshes are gltf primitives, [`MorphWeights`] on the bevy node entity
|
||||||
|
/// should be inherited by children meshes.
|
||||||
|
///
|
||||||
|
/// Only direct children are updated, to fulfill the expectations of glTF spec.
|
||||||
|
#[cfg(feature = "morph")]
|
||||||
|
pub fn inherit_weights(
|
||||||
|
morph_nodes: Query<(&Children, &MorphWeights), (Without<Mesh3d>, Changed<MorphWeights>)>,
|
||||||
|
mut morph_primitives: Query<&mut MeshMorphWeights, With<Mesh3d>>,
|
||||||
|
) {
|
||||||
|
for (children, parent_weights) in &morph_nodes {
|
||||||
|
let mut iter = morph_primitives.iter_many_mut(children);
|
||||||
|
while let Some(mut child_weight) = iter.fetch_next() {
|
||||||
|
child_weight.clear_weights();
|
||||||
|
child_weight.extend_weights(parent_weights.weights());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The render world representation of a [`Mesh`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RenderMesh {
|
||||||
|
/// The number of vertices in the mesh.
|
||||||
|
pub vertex_count: u32,
|
||||||
|
|
||||||
|
/// Morph targets for the mesh, if present.
|
||||||
|
#[cfg(feature = "morph")]
|
||||||
|
pub morph_targets: Option<crate::render_resource::TextureView>,
|
||||||
|
|
||||||
|
/// Information about the mesh data buffers, including whether the mesh uses
|
||||||
|
/// indices or not.
|
||||||
|
pub buffer_info: RenderMeshBufferInfo,
|
||||||
|
|
||||||
|
/// Precomputed pipeline key bits for this mesh.
|
||||||
|
pub key_bits: BaseMeshPipelineKey,
|
||||||
|
|
||||||
|
/// A reference to the vertex buffer layout.
|
||||||
|
///
|
||||||
|
/// Combined with [`RenderMesh::buffer_info`], this specifies the complete
|
||||||
|
/// layout of the buffers associated with this mesh.
|
||||||
|
pub layout: MeshVertexBufferLayoutRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderMesh {
|
||||||
|
/// Returns the primitive topology of this mesh (triangles, triangle strips,
|
||||||
|
/// etc.)
|
||||||
|
#[inline]
|
||||||
|
pub fn primitive_topology(&self) -> PrimitiveTopology {
|
||||||
|
self.key_bits.primitive_topology()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this mesh uses an index buffer or false otherwise.
|
||||||
|
#[inline]
|
||||||
|
pub fn indexed(&self) -> bool {
|
||||||
|
matches!(self.buffer_info, RenderMeshBufferInfo::Indexed { .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The index/vertex buffer info of a [`RenderMesh`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum RenderMeshBufferInfo {
|
||||||
|
Indexed {
|
||||||
|
count: u32,
|
||||||
|
index_format: IndexFormat,
|
||||||
|
},
|
||||||
|
NonIndexed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAsset for RenderMesh {
|
||||||
|
type SourceAsset = Mesh;
|
||||||
|
type Param = (
|
||||||
|
SRes<RenderAssets<GpuImage>>,
|
||||||
|
SResMut<MeshVertexBufferLayouts>,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn asset_usage(mesh: &Self::SourceAsset) -> RenderAssetUsages {
|
||||||
|
mesh.asset_usage
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_gpu_data(
|
||||||
|
source: &mut Self::SourceAsset,
|
||||||
|
_previous_gpu_asset: Option<&Self>,
|
||||||
|
) -> Result<Self::SourceAsset, AssetExtractionError> {
|
||||||
|
source
|
||||||
|
.take_gpu_data()
|
||||||
|
.map_err(|_| AssetExtractionError::AlreadyExtracted)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn byte_len(mesh: &Self::SourceAsset) -> Option<usize> {
|
||||||
|
let mut vertex_size = 0;
|
||||||
|
for attribute_data in mesh.attributes() {
|
||||||
|
let vertex_format = attribute_data.0.format;
|
||||||
|
vertex_size += vertex_format.size() as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vertex_count = mesh.count_vertices();
|
||||||
|
let index_bytes = mesh.get_index_buffer_bytes().map(<[_]>::len).unwrap_or(0);
|
||||||
|
Some(vertex_size * vertex_count + index_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the extracted mesh into a [`RenderMesh`].
|
||||||
|
fn prepare_asset(
|
||||||
|
mesh: Self::SourceAsset,
|
||||||
|
_: AssetId<Self::SourceAsset>,
|
||||||
|
(_images, mesh_vertex_buffer_layouts): &mut SystemParamItem<Self::Param>,
|
||||||
|
_: Option<&Self>,
|
||||||
|
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||||
|
#[cfg(feature = "morph")]
|
||||||
|
let morph_targets = match mesh.morph_targets() {
|
||||||
|
Some(mt) => {
|
||||||
|
let Some(target_image) = _images.get(mt) else {
|
||||||
|
return Err(PrepareAssetError::RetryNextUpdate(mesh));
|
||||||
|
};
|
||||||
|
Some(target_image.texture_view.clone())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let buffer_info = match mesh.indices() {
|
||||||
|
Some(indices) => RenderMeshBufferInfo::Indexed {
|
||||||
|
count: indices.len() as u32,
|
||||||
|
index_format: indices.into(),
|
||||||
|
},
|
||||||
|
None => RenderMeshBufferInfo::NonIndexed,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mesh_vertex_buffer_layout =
|
||||||
|
mesh.get_mesh_vertex_buffer_layout(mesh_vertex_buffer_layouts);
|
||||||
|
|
||||||
|
let key_bits = BaseMeshPipelineKey::from_primitive_topology(mesh.primitive_topology());
|
||||||
|
#[cfg(feature = "morph")]
|
||||||
|
let key_bits = if mesh.morph_targets().is_some() {
|
||||||
|
key_bits | BaseMeshPipelineKey::MORPH_TARGETS
|
||||||
|
} else {
|
||||||
|
key_bits
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(RenderMesh {
|
||||||
|
vertex_count: mesh.count_vertices() as u32,
|
||||||
|
buffer_info,
|
||||||
|
key_bits,
|
||||||
|
layout: mesh_vertex_buffer_layout,
|
||||||
|
#[cfg(feature = "morph")]
|
||||||
|
morph_targets,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
204
third_party/bevy_render/src/pipelined_rendering.rs
vendored
Normal file
204
third_party/bevy_render/src/pipelined_rendering.rs
vendored
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
use async_channel::{Receiver, Sender};
|
||||||
|
|
||||||
|
use bevy_app::{App, AppExit, AppLabel, Plugin, SubApp};
|
||||||
|
use bevy_ecs::{
|
||||||
|
resource::Resource,
|
||||||
|
schedule::MainThreadExecutor,
|
||||||
|
world::{Mut, World},
|
||||||
|
};
|
||||||
|
use bevy_tasks::ComputeTaskPool;
|
||||||
|
|
||||||
|
use crate::RenderApp;
|
||||||
|
|
||||||
|
/// A Label for the sub app that runs the parts of pipelined rendering that need to run on the main thread.
|
||||||
|
///
|
||||||
|
/// The Main schedule of this app can be used to run logic after the render schedule starts, but
|
||||||
|
/// before I/O processing. This can be useful for something like frame pacing.
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]
|
||||||
|
pub struct RenderExtractApp;
|
||||||
|
|
||||||
|
/// Channels used by the main app to send and receive the render app.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct RenderAppChannels {
|
||||||
|
app_to_render_sender: Sender<SubApp>,
|
||||||
|
render_to_app_receiver: Receiver<SubApp>,
|
||||||
|
render_app_in_render_thread: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAppChannels {
|
||||||
|
/// Create a `RenderAppChannels` from a [`async_channel::Receiver`] and [`async_channel::Sender`]
|
||||||
|
pub fn new(
|
||||||
|
app_to_render_sender: Sender<SubApp>,
|
||||||
|
render_to_app_receiver: Receiver<SubApp>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
app_to_render_sender,
|
||||||
|
render_to_app_receiver,
|
||||||
|
render_app_in_render_thread: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the `render_app` to the rendering thread.
|
||||||
|
pub fn send_blocking(&mut self, render_app: SubApp) {
|
||||||
|
self.app_to_render_sender.send_blocking(render_app).unwrap();
|
||||||
|
self.render_app_in_render_thread = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive the `render_app` from the rendering thread.
|
||||||
|
/// Return `None` if the render thread has panicked.
|
||||||
|
pub async fn recv(&mut self) -> Option<SubApp> {
|
||||||
|
let render_app = self.render_to_app_receiver.recv().await.ok()?;
|
||||||
|
self.render_app_in_render_thread = false;
|
||||||
|
Some(render_app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for RenderAppChannels {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.render_app_in_render_thread {
|
||||||
|
// Any non-send data in the render world was initialized on the main thread.
|
||||||
|
// So on dropping the main world and ending the app, we block and wait for
|
||||||
|
// the render world to return to drop it. Which allows the non-send data
|
||||||
|
// drop methods to run on the correct thread.
|
||||||
|
self.render_to_app_receiver.recv_blocking().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`PipelinedRenderingPlugin`] can be added to your application to enable pipelined rendering.
|
||||||
|
///
|
||||||
|
/// This moves rendering into a different thread, so that the Nth frame's rendering can
|
||||||
|
/// be run at the same time as the N + 1 frame's simulation.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// |--------------------|--------------------|--------------------|--------------------|
|
||||||
|
/// | simulation thread | frame 1 simulation | frame 2 simulation | frame 3 simulation |
|
||||||
|
/// |--------------------|--------------------|--------------------|--------------------|
|
||||||
|
/// | rendering thread | | frame 1 rendering | frame 2 rendering |
|
||||||
|
/// |--------------------|--------------------|--------------------|--------------------|
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must
|
||||||
|
/// be added after that plugin. If it is not added after, the plugin will do nothing.
|
||||||
|
///
|
||||||
|
/// A single frame of execution looks something like below
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// |---------------------------------------------------------------------------|
|
||||||
|
/// | | | RenderExtractApp schedule | winit events | main schedule |
|
||||||
|
/// | sync | extract |----------------------------------------------------------|
|
||||||
|
/// | | | extract commands | rendering schedule |
|
||||||
|
/// |---------------------------------------------------------------------------|
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - `sync` is the step where the entity-entity mapping between the main and render world is updated.
|
||||||
|
/// This is run on the main app's thread. For more information checkout [`SyncWorldPlugin`].
|
||||||
|
/// - `extract` is the step where data is copied from the main world to the render world.
|
||||||
|
/// This is run on the main app's thread.
|
||||||
|
/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the
|
||||||
|
/// main schedule can start sooner.
|
||||||
|
/// - Then the `rendering schedule` is run. See [`RenderSystems`](crate::RenderSystems) for the standard steps in this process.
|
||||||
|
/// - In parallel to the rendering thread the [`RenderExtractApp`] schedule runs. By
|
||||||
|
/// default, this schedule is empty. But it is useful if you need something to run before I/O processing.
|
||||||
|
/// - Next all the `winit events` are processed.
|
||||||
|
/// - And finally the `main app schedule` is run.
|
||||||
|
/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again.
|
||||||
|
///
|
||||||
|
/// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PipelinedRenderingPlugin;
|
||||||
|
|
||||||
|
impl Plugin for PipelinedRenderingPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
// Don't add RenderExtractApp if RenderApp isn't initialized.
|
||||||
|
if app.get_sub_app(RenderApp).is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.insert_resource(MainThreadExecutor::new());
|
||||||
|
|
||||||
|
let mut sub_app = SubApp::new();
|
||||||
|
sub_app.set_extract(renderer_extract);
|
||||||
|
app.insert_sub_app(RenderExtractApp, sub_app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets up the render thread and inserts resources into the main app used for controlling the render thread.
|
||||||
|
fn cleanup(&self, app: &mut App) {
|
||||||
|
// skip setting up when headless
|
||||||
|
if app.get_sub_app(RenderExtractApp).is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::<SubApp>(1);
|
||||||
|
let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::<SubApp>(1);
|
||||||
|
|
||||||
|
let mut render_app = app
|
||||||
|
.remove_sub_app(RenderApp)
|
||||||
|
.expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before PipelinedRenderingPlugin");
|
||||||
|
|
||||||
|
// clone main thread executor to render world
|
||||||
|
let executor = app.world().get_resource::<MainThreadExecutor>().unwrap();
|
||||||
|
render_app.world_mut().insert_resource(executor.clone());
|
||||||
|
|
||||||
|
render_to_app_sender.send_blocking(render_app).unwrap();
|
||||||
|
|
||||||
|
app.insert_resource(RenderAppChannels::new(
|
||||||
|
app_to_render_sender,
|
||||||
|
render_to_app_receiver,
|
||||||
|
));
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
#[cfg(feature = "trace")]
|
||||||
|
let _span = tracing::info_span!("render thread").entered();
|
||||||
|
|
||||||
|
let compute_task_pool = ComputeTaskPool::get();
|
||||||
|
loop {
|
||||||
|
// run a scope here to allow main world to use this thread while it's waiting for the render app
|
||||||
|
let sent_app = compute_task_pool
|
||||||
|
.scope(|s| {
|
||||||
|
s.spawn(async { app_to_render_receiver.recv().await });
|
||||||
|
})
|
||||||
|
.pop();
|
||||||
|
let Some(Ok(mut render_app)) = sent_app else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
#[cfg(feature = "trace")]
|
||||||
|
let _sub_app_span = tracing::info_span!("sub app", name = ?RenderApp).entered();
|
||||||
|
render_app.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if render_to_app_sender.send_blocking(render_app).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("exiting pipelined rendering thread");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function waits for the rendering world to be received,
|
||||||
|
// runs extract, and then sends the rendering world back to the render thread.
|
||||||
|
fn renderer_extract(app_world: &mut World, _world: &mut World) {
|
||||||
|
app_world.resource_scope(|world, main_thread_executor: Mut<MainThreadExecutor>| {
|
||||||
|
world.resource_scope(|world, mut render_channels: Mut<RenderAppChannels>| {
|
||||||
|
// we use a scope here to run any main thread tasks that the render world still needs to run
|
||||||
|
// while we wait for the render world to be received.
|
||||||
|
if let Some(mut render_app) = ComputeTaskPool::get()
|
||||||
|
.scope_with_executor(true, Some(&*main_thread_executor.0), |s| {
|
||||||
|
s.spawn(async { render_channels.recv().await });
|
||||||
|
})
|
||||||
|
.pop()
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
render_app.extract(world);
|
||||||
|
|
||||||
|
render_channels.send_blocking(render_app);
|
||||||
|
} else {
|
||||||
|
// Renderer thread panicked
|
||||||
|
world.write_message(AppExit::error());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
542
third_party/bevy_render/src/render_asset.rs
vendored
Normal file
542
third_party/bevy_render/src/render_asset.rs
vendored
Normal file
@ -0,0 +1,542 @@
|
|||||||
|
use crate::{
|
||||||
|
render_resource::AsBindGroupError, Extract, ExtractSchedule, MainWorld, Render, RenderApp,
|
||||||
|
RenderSystems, Res,
|
||||||
|
};
|
||||||
|
use bevy_app::{App, Plugin, SubApp};
|
||||||
|
use bevy_asset::{Asset, AssetEvent, AssetId, Assets, RenderAssetUsages};
|
||||||
|
use bevy_ecs::{
|
||||||
|
prelude::{Commands, IntoScheduleConfigs, MessageReader, ResMut, Resource},
|
||||||
|
schedule::{ScheduleConfigs, SystemSet},
|
||||||
|
system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState},
|
||||||
|
world::{FromWorld, Mut},
|
||||||
|
};
|
||||||
|
use bevy_platform::collections::{HashMap, HashSet};
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PrepareAssetError<E: Send + Sync + 'static> {
|
||||||
|
#[error("Failed to prepare asset")]
|
||||||
|
RetryNextUpdate(E),
|
||||||
|
#[error("Failed to build bind group: {0}")]
|
||||||
|
AsBindGroupError(AsBindGroupError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The system set during which we extract modified assets to the render world.
|
||||||
|
#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
|
||||||
|
pub struct AssetExtractionSystems;
|
||||||
|
|
||||||
|
/// Error returned when an asset due for extraction has already been extracted
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AssetExtractionError {
|
||||||
|
#[error("The asset has already been extracted")]
|
||||||
|
AlreadyExtracted,
|
||||||
|
#[error("The asset type does not support extraction. To clone the asset to the renderworld, use `RenderAssetUsages::default()`")]
|
||||||
|
NoExtractionImplementation,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes how an asset gets extracted and prepared for rendering.
|
||||||
|
///
|
||||||
|
/// In the [`ExtractSchedule`] step the [`RenderAsset::SourceAsset`] is transferred
|
||||||
|
/// from the "main world" into the "render world".
|
||||||
|
///
|
||||||
|
/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset
|
||||||
|
/// is transformed into its GPU-representation of type [`RenderAsset`].
|
||||||
|
pub trait RenderAsset: Send + Sync + 'static + Sized {
|
||||||
|
/// The representation of the asset in the "main world".
|
||||||
|
type SourceAsset: Asset + Clone;
|
||||||
|
|
||||||
|
/// Specifies all ECS data required by [`RenderAsset::prepare_asset`].
|
||||||
|
///
|
||||||
|
/// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`].
|
||||||
|
type Param: SystemParam;
|
||||||
|
|
||||||
|
/// Whether or not to unload the asset after extracting it to the render world.
|
||||||
|
#[inline]
|
||||||
|
fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages {
|
||||||
|
RenderAssetUsages::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size of the data the asset will upload to the gpu. Specifying a return value
|
||||||
|
/// will allow the asset to be throttled via [`RenderAssetBytesPerFrame`].
|
||||||
|
#[inline]
|
||||||
|
#[expect(
|
||||||
|
unused_variables,
|
||||||
|
reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion."
|
||||||
|
)]
|
||||||
|
fn byte_len(source_asset: &Self::SourceAsset) -> Option<usize> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares the [`RenderAsset::SourceAsset`] for the GPU by transforming it into a [`RenderAsset`].
|
||||||
|
///
|
||||||
|
/// ECS data may be accessed via `param`.
|
||||||
|
fn prepare_asset(
|
||||||
|
source_asset: Self::SourceAsset,
|
||||||
|
asset_id: AssetId<Self::SourceAsset>,
|
||||||
|
param: &mut SystemParamItem<Self::Param>,
|
||||||
|
previous_asset: Option<&Self>,
|
||||||
|
) -> Result<Self, PrepareAssetError<Self::SourceAsset>>;
|
||||||
|
|
||||||
|
/// Called whenever the [`RenderAsset::SourceAsset`] has been removed.
|
||||||
|
///
|
||||||
|
/// You can implement this method if you need to access ECS data (via
|
||||||
|
/// `_param`) in order to perform cleanup tasks when the asset is removed.
|
||||||
|
///
|
||||||
|
/// The default implementation does nothing.
|
||||||
|
fn unload_asset(
|
||||||
|
_source_asset: AssetId<Self::SourceAsset>,
|
||||||
|
_param: &mut SystemParamItem<Self::Param>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make a copy of the asset to be moved to the `RenderWorld` / gpu. Heavy internal data (pixels, vertex attributes)
|
||||||
|
/// should be moved into the copy, leaving this asset with only metadata.
|
||||||
|
/// An error may be returned to indicate that the asset has already been extracted, and should not
|
||||||
|
/// have been modified on the CPU side (as it cannot be transferred to GPU again).
|
||||||
|
/// The previous GPU asset is also provided, which can be used to check if the modification is valid.
|
||||||
|
fn take_gpu_data(
|
||||||
|
_source: &mut Self::SourceAsset,
|
||||||
|
_previous_gpu_asset: Option<&Self>,
|
||||||
|
) -> Result<Self::SourceAsset, AssetExtractionError> {
|
||||||
|
Err(AssetExtractionError::NoExtractionImplementation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This plugin extracts the changed assets from the "app world" into the "render world"
|
||||||
|
/// and prepares them for the GPU. They can then be accessed from the [`RenderAssets`] resource.
|
||||||
|
///
|
||||||
|
/// Therefore it sets up the [`ExtractSchedule`] and
|
||||||
|
/// [`RenderSystems::PrepareAssets`] steps for the specified [`RenderAsset`].
|
||||||
|
///
|
||||||
|
/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until
|
||||||
|
/// `prepare_assets::<AFTER>` has completed. This allows the `prepare_asset` function to depend on another
|
||||||
|
/// prepared [`RenderAsset`], for example `Mesh::prepare_asset` relies on `RenderAssets::<GpuImage>` for morph
|
||||||
|
/// targets, so the plugin is created as `RenderAssetPlugin::<RenderMesh, GpuImage>::default()`.
|
||||||
|
pub struct RenderAssetPlugin<A: RenderAsset, AFTER: RenderAssetDependency + 'static = ()> {
|
||||||
|
phantom: PhantomData<fn() -> (A, AFTER)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset, AFTER: RenderAssetDependency + 'static> Default
|
||||||
|
for RenderAssetPlugin<A, AFTER>
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
phantom: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset, AFTER: RenderAssetDependency + 'static> Plugin
|
||||||
|
for RenderAssetPlugin<A, AFTER>
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<CachedExtractRenderAssetSystemState<A>>();
|
||||||
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||||
|
render_app
|
||||||
|
.init_resource::<ExtractedAssets<A>>()
|
||||||
|
.init_resource::<RenderAssets<A>>()
|
||||||
|
.init_resource::<PrepareNextFrameAssets<A>>()
|
||||||
|
.add_systems(
|
||||||
|
ExtractSchedule,
|
||||||
|
extract_render_asset::<A>.in_set(AssetExtractionSystems),
|
||||||
|
);
|
||||||
|
AFTER::register_system(
|
||||||
|
render_app,
|
||||||
|
prepare_assets::<A>.in_set(RenderSystems::PrepareAssets),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to allow specifying dependencies between render assets
|
||||||
|
pub trait RenderAssetDependency {
|
||||||
|
fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAssetDependency for () {
|
||||||
|
fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
|
||||||
|
render_app.add_systems(Render, system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset> RenderAssetDependency for A {
|
||||||
|
fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
|
||||||
|
render_app.add_systems(Render, system.after(prepare_assets::<A>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporarily stores the extracted and removed assets of the current frame.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ExtractedAssets<A: RenderAsset> {
|
||||||
|
/// The assets extracted this frame.
|
||||||
|
///
|
||||||
|
/// These are assets that were either added or modified this frame.
|
||||||
|
pub extracted: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
|
||||||
|
|
||||||
|
/// IDs of the assets that were removed this frame.
|
||||||
|
///
|
||||||
|
/// These assets will not be present in [`ExtractedAssets::extracted`].
|
||||||
|
pub removed: HashSet<AssetId<A::SourceAsset>>,
|
||||||
|
|
||||||
|
/// IDs of the assets that were modified this frame.
|
||||||
|
pub modified: HashSet<AssetId<A::SourceAsset>>,
|
||||||
|
|
||||||
|
/// IDs of the assets that were added this frame.
|
||||||
|
pub added: HashSet<AssetId<A::SourceAsset>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset> Default for ExtractedAssets<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
extracted: Default::default(),
|
||||||
|
removed: Default::default(),
|
||||||
|
modified: Default::default(),
|
||||||
|
added: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores all GPU representations ([`RenderAsset`])
|
||||||
|
/// of [`RenderAsset::SourceAsset`] as long as they exist.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct RenderAssets<A: RenderAsset>(HashMap<AssetId<A::SourceAsset>, A>);
|
||||||
|
|
||||||
|
impl<A: RenderAsset> Default for RenderAssets<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset> RenderAssets<A> {
|
||||||
|
pub fn get(&self, id: impl Into<AssetId<A::SourceAsset>>) -> Option<&A> {
|
||||||
|
self.0.get(&id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, id: impl Into<AssetId<A::SourceAsset>>) -> Option<&mut A> {
|
||||||
|
self.0.get_mut(&id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, id: impl Into<AssetId<A::SourceAsset>>, value: A) -> Option<A> {
|
||||||
|
self.0.insert(id.into(), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, id: impl Into<AssetId<A::SourceAsset>>) -> Option<A> {
|
||||||
|
self.0.remove(&id.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (AssetId<A::SourceAsset>, &A)> {
|
||||||
|
self.0.iter().map(|(k, v)| (*k, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_mut(&mut self) -> impl Iterator<Item = (AssetId<A::SourceAsset>, &mut A)> {
|
||||||
|
self.0.iter_mut().map(|(k, v)| (*k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct CachedExtractRenderAssetSystemState<A: RenderAsset> {
|
||||||
|
state: SystemState<(
|
||||||
|
MessageReader<'static, 'static, AssetEvent<A::SourceAsset>>,
|
||||||
|
ResMut<'static, Assets<A::SourceAsset>>,
|
||||||
|
Option<Res<'static, RenderAssets<A>>>,
|
||||||
|
)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset> FromWorld for CachedExtractRenderAssetSystemState<A> {
|
||||||
|
fn from_world(world: &mut bevy_ecs::world::World) -> Self {
|
||||||
|
Self {
|
||||||
|
state: SystemState::new(world),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system extracts all created or modified assets of the corresponding [`RenderAsset::SourceAsset`] type
|
||||||
|
/// into the "render world".
|
||||||
|
pub(crate) fn extract_render_asset<A: RenderAsset>(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut main_world: ResMut<MainWorld>,
|
||||||
|
) {
|
||||||
|
main_world.resource_scope(
|
||||||
|
|world, mut cached_state: Mut<CachedExtractRenderAssetSystemState<A>>| {
|
||||||
|
let (mut events, mut assets, maybe_render_assets) = cached_state.state.get_mut(world);
|
||||||
|
|
||||||
|
let mut needs_extracting = <HashSet<_>>::default();
|
||||||
|
let mut removed = <HashSet<_>>::default();
|
||||||
|
let mut modified = <HashSet<_>>::default();
|
||||||
|
|
||||||
|
for event in events.read() {
|
||||||
|
#[expect(
|
||||||
|
clippy::match_same_arms,
|
||||||
|
reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."
|
||||||
|
)]
|
||||||
|
match event {
|
||||||
|
AssetEvent::Added { id } => {
|
||||||
|
needs_extracting.insert(*id);
|
||||||
|
}
|
||||||
|
AssetEvent::Modified { id } => {
|
||||||
|
needs_extracting.insert(*id);
|
||||||
|
modified.insert(*id);
|
||||||
|
}
|
||||||
|
AssetEvent::Removed { .. } => {
|
||||||
|
// We don't care that the asset was removed from Assets<T> in the main world.
|
||||||
|
// An asset is only removed from RenderAssets<T> when its last handle is dropped (AssetEvent::Unused).
|
||||||
|
}
|
||||||
|
AssetEvent::Unused { id } => {
|
||||||
|
needs_extracting.remove(id);
|
||||||
|
modified.remove(id);
|
||||||
|
removed.insert(*id);
|
||||||
|
}
|
||||||
|
AssetEvent::LoadedWithDependencies { .. } => {
|
||||||
|
// TODO: handle this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut extracted_assets = Vec::new();
|
||||||
|
let mut added = <HashSet<_>>::default();
|
||||||
|
for id in needs_extracting.drain() {
|
||||||
|
if let Some(asset) = assets.get(id) {
|
||||||
|
let asset_usage = A::asset_usage(asset);
|
||||||
|
if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) {
|
||||||
|
if asset_usage == RenderAssetUsages::RENDER_WORLD {
|
||||||
|
if let Some(asset) = assets.get_mut_untracked(id) {
|
||||||
|
let previous_asset = maybe_render_assets.as_ref().and_then(|render_assets| render_assets.get(id));
|
||||||
|
match A::take_gpu_data(asset, previous_asset) {
|
||||||
|
Ok(gpu_data_asset) => {
|
||||||
|
extracted_assets.push((id, gpu_data_asset));
|
||||||
|
added.insert(id);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("{} with RenderAssetUsages == RENDER_WORLD cannot be extracted: {e}", core::any::type_name::<A>());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extracted_assets.push((id, asset.clone()));
|
||||||
|
added.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.insert_resource(ExtractedAssets::<A> {
|
||||||
|
extracted: extracted_assets,
|
||||||
|
removed,
|
||||||
|
modified,
|
||||||
|
added,
|
||||||
|
});
|
||||||
|
cached_state.state.apply(world);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: consider storing inside system?
|
||||||
|
/// All assets that should be prepared next frame.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct PrepareNextFrameAssets<A: RenderAsset> {
|
||||||
|
assets: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: RenderAsset> Default for PrepareNextFrameAssets<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
assets: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This system prepares all assets of the corresponding [`RenderAsset::SourceAsset`] type
|
||||||
|
/// which where extracted this frame for the GPU.
|
||||||
|
pub fn prepare_assets<A: RenderAsset>(
|
||||||
|
mut extracted_assets: ResMut<ExtractedAssets<A>>,
|
||||||
|
mut render_assets: ResMut<RenderAssets<A>>,
|
||||||
|
mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,
|
||||||
|
param: StaticSystemParam<<A as RenderAsset>::Param>,
|
||||||
|
bpf: Res<RenderAssetBytesPerFrameLimiter>,
|
||||||
|
) {
|
||||||
|
let mut wrote_asset_count = 0;
|
||||||
|
|
||||||
|
let mut param = param.into_inner();
|
||||||
|
let queued_assets = core::mem::take(&mut prepare_next_frame.assets);
|
||||||
|
for (id, extracted_asset) in queued_assets {
|
||||||
|
if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) {
|
||||||
|
// skip previous frame's assets that have been removed or updated
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
|
||||||
|
// we could check if available bytes > byte_len here, but we want to make some
|
||||||
|
// forward progress even if the asset is larger than the max bytes per frame.
|
||||||
|
// this way we always write at least one (sized) asset per frame.
|
||||||
|
// in future we could also consider partial asset uploads.
|
||||||
|
if bpf.exhausted() {
|
||||||
|
prepare_next_frame.assets.push((id, extracted_asset));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let previous_asset = render_assets.get(id);
|
||||||
|
match A::prepare_asset(extracted_asset, id, &mut param, previous_asset) {
|
||||||
|
Ok(prepared_asset) => {
|
||||||
|
render_assets.insert(id, prepared_asset);
|
||||||
|
bpf.write_bytes(write_bytes);
|
||||||
|
wrote_asset_count += 1;
|
||||||
|
}
|
||||||
|
Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
|
||||||
|
prepare_next_frame.assets.push((id, extracted_asset));
|
||||||
|
}
|
||||||
|
Err(PrepareAssetError::AsBindGroupError(e)) => {
|
||||||
|
error!(
|
||||||
|
"{} Bind group construction failed: {e}",
|
||||||
|
core::any::type_name::<A>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for removed in extracted_assets.removed.drain() {
|
||||||
|
render_assets.remove(removed);
|
||||||
|
A::unload_asset(removed, &mut param);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (id, extracted_asset) in extracted_assets.extracted.drain(..) {
|
||||||
|
// we remove previous here to ensure that if we are updating the asset then
|
||||||
|
// any users will not see the old asset after a new asset is extracted,
|
||||||
|
// even if the new asset is not yet ready or we are out of bytes to write.
|
||||||
|
let previous_asset = render_assets.remove(id);
|
||||||
|
|
||||||
|
let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
|
||||||
|
if bpf.exhausted() {
|
||||||
|
prepare_next_frame.assets.push((id, extracted_asset));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
match A::prepare_asset(extracted_asset, id, &mut param, previous_asset.as_ref()) {
|
||||||
|
Ok(prepared_asset) => {
|
||||||
|
render_assets.insert(id, prepared_asset);
|
||||||
|
bpf.write_bytes(write_bytes);
|
||||||
|
wrote_asset_count += 1;
|
||||||
|
}
|
||||||
|
Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
|
||||||
|
prepare_next_frame.assets.push((id, extracted_asset));
|
||||||
|
}
|
||||||
|
Err(PrepareAssetError::AsBindGroupError(e)) => {
|
||||||
|
error!(
|
||||||
|
"{} Bind group construction failed: {e}",
|
||||||
|
core::any::type_name::<A>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bpf.exhausted() && !prepare_next_frame.assets.is_empty() {
|
||||||
|
debug!(
|
||||||
|
"{} write budget exhausted with {} assets remaining (wrote {})",
|
||||||
|
core::any::type_name::<A>(),
|
||||||
|
prepare_next_frame.assets.len(),
|
||||||
|
wrote_asset_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_render_asset_bytes_per_frame(
|
||||||
|
mut bpf_limiter: ResMut<RenderAssetBytesPerFrameLimiter>,
|
||||||
|
) {
|
||||||
|
bpf_limiter.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_render_asset_bytes_per_frame(
|
||||||
|
bpf: Extract<Res<RenderAssetBytesPerFrame>>,
|
||||||
|
mut bpf_limiter: ResMut<RenderAssetBytesPerFrameLimiter>,
|
||||||
|
) {
|
||||||
|
bpf_limiter.max_bytes = bpf.max_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A resource that defines the amount of data allowed to be transferred from CPU to GPU
|
||||||
|
/// each frame, preventing choppy frames at the cost of waiting longer for GPU assets
|
||||||
|
/// to become available.
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct RenderAssetBytesPerFrame {
|
||||||
|
pub max_bytes: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAssetBytesPerFrame {
|
||||||
|
/// `max_bytes`: the number of bytes to write per frame.
|
||||||
|
///
|
||||||
|
/// This is a soft limit: only full assets are written currently, uploading stops
|
||||||
|
/// after the first asset that exceeds the limit.
|
||||||
|
///
|
||||||
|
/// To participate, assets should implement [`RenderAsset::byte_len`]. If the default
|
||||||
|
/// is not overridden, the assets are assumed to be small enough to upload without restriction.
|
||||||
|
pub fn new(max_bytes: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
max_bytes: Some(max_bytes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A render-world resource that facilitates limiting the data transferred from CPU to GPU
|
||||||
|
/// each frame, preventing choppy frames at the cost of waiting longer for GPU assets
|
||||||
|
/// to become available.
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct RenderAssetBytesPerFrameLimiter {
|
||||||
|
/// Populated by [`RenderAssetBytesPerFrame`] during extraction.
|
||||||
|
pub max_bytes: Option<usize>,
|
||||||
|
/// Bytes written this frame.
|
||||||
|
pub bytes_written: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderAssetBytesPerFrameLimiter {
|
||||||
|
/// Reset the available bytes. Called once per frame during extraction by [`crate::RenderPlugin`].
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
if self.max_bytes.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.bytes_written.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check how many bytes are available for writing.
|
||||||
|
pub fn available_bytes(&self, required_bytes: usize) -> usize {
|
||||||
|
if let Some(max_bytes) = self.max_bytes {
|
||||||
|
let total_bytes = self
|
||||||
|
.bytes_written
|
||||||
|
.fetch_add(required_bytes, Ordering::Relaxed);
|
||||||
|
|
||||||
|
// The bytes available is the inverse of the amount we overshot max_bytes
|
||||||
|
if total_bytes >= max_bytes {
|
||||||
|
required_bytes.saturating_sub(total_bytes - max_bytes)
|
||||||
|
} else {
|
||||||
|
required_bytes
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
required_bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decreases the available bytes for the current frame.
|
||||||
|
pub(crate) fn write_bytes(&self, bytes: usize) {
|
||||||
|
if self.max_bytes.is_some() && bytes > 0 {
|
||||||
|
self.bytes_written.fetch_add(bytes, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if there are no remaining bytes available for writing this frame.
|
||||||
|
pub(crate) fn exhausted(&self) -> bool {
|
||||||
|
if let Some(max_bytes) = self.max_bytes {
|
||||||
|
let bytes_written = self.bytes_written.load(Ordering::Relaxed);
|
||||||
|
bytes_written >= max_bytes
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
third_party/bevy_render/src/render_graph/app.rs
vendored
Normal file
174
third_party/bevy_render/src/render_graph/app.rs
vendored
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
use bevy_app::{App, SubApp};
|
||||||
|
use bevy_ecs::world::{FromWorld, World};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use super::{IntoRenderNodeArray, Node, RenderGraph, RenderLabel, RenderSubGraph};
|
||||||
|
|
||||||
|
/// Adds common [`RenderGraph`] operations to [`SubApp`] (and [`App`]).
|
||||||
|
pub trait RenderGraphExt {
|
||||||
|
// Add a sub graph to the [`RenderGraph`]
|
||||||
|
fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self;
|
||||||
|
/// Add a [`Node`] to the [`RenderGraph`]:
|
||||||
|
/// * Create the [`Node`] using the [`FromWorld`] implementation
|
||||||
|
/// * Add it to the graph
|
||||||
|
fn add_render_graph_node<T: Node + FromWorld>(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
node_label: impl RenderLabel,
|
||||||
|
) -> &mut Self;
|
||||||
|
/// Automatically add the required node edges based on the given ordering
|
||||||
|
fn add_render_graph_edges<const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
edges: impl IntoRenderNodeArray<N>,
|
||||||
|
) -> &mut Self;
|
||||||
|
|
||||||
|
/// Add node edge to the specified graph
|
||||||
|
fn add_render_graph_edge(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
output_node: impl RenderLabel,
|
||||||
|
input_node: impl RenderLabel,
|
||||||
|
) -> &mut Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderGraphExt for World {
|
||||||
|
fn add_render_graph_node<T: Node + FromWorld>(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
node_label: impl RenderLabel,
|
||||||
|
) -> &mut Self {
|
||||||
|
let sub_graph = sub_graph.intern();
|
||||||
|
let node = T::from_world(self);
|
||||||
|
let mut render_graph = self.get_resource_mut::<RenderGraph>().expect(
|
||||||
|
"RenderGraph not found. Make sure you are using add_render_graph_node on the RenderApp",
|
||||||
|
);
|
||||||
|
if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) {
|
||||||
|
graph.add_node(node_label, node);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Tried adding a render graph node to {sub_graph:?} but the sub graph doesn't exist"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn add_render_graph_edges<const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
edges: impl IntoRenderNodeArray<N>,
|
||||||
|
) -> &mut Self {
|
||||||
|
let sub_graph = sub_graph.intern();
|
||||||
|
let mut render_graph = self.get_resource_mut::<RenderGraph>().expect(
|
||||||
|
"RenderGraph not found. Make sure you are using add_render_graph_edges on the RenderApp",
|
||||||
|
);
|
||||||
|
if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) {
|
||||||
|
graph.add_node_edges(edges);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Tried adding render graph edges to {sub_graph:?} but the sub graph doesn't exist"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_render_graph_edge(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
output_node: impl RenderLabel,
|
||||||
|
input_node: impl RenderLabel,
|
||||||
|
) -> &mut Self {
|
||||||
|
let sub_graph = sub_graph.intern();
|
||||||
|
let mut render_graph = self.get_resource_mut::<RenderGraph>().expect(
|
||||||
|
"RenderGraph not found. Make sure you are using add_render_graph_edge on the RenderApp",
|
||||||
|
);
|
||||||
|
if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) {
|
||||||
|
graph.add_node_edge(output_node, input_node);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Tried adding a render graph edge to {sub_graph:?} but the sub graph doesn't exist"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self {
|
||||||
|
let mut render_graph = self.get_resource_mut::<RenderGraph>().expect(
|
||||||
|
"RenderGraph not found. Make sure you are using add_render_sub_graph on the RenderApp",
|
||||||
|
);
|
||||||
|
render_graph.add_sub_graph(sub_graph, RenderGraph::default());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderGraphExt for SubApp {
|
||||||
|
fn add_render_graph_node<T: Node + FromWorld>(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
node_label: impl RenderLabel,
|
||||||
|
) -> &mut Self {
|
||||||
|
World::add_render_graph_node::<T>(self.world_mut(), sub_graph, node_label);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_render_graph_edge(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
output_node: impl RenderLabel,
|
||||||
|
input_node: impl RenderLabel,
|
||||||
|
) -> &mut Self {
|
||||||
|
World::add_render_graph_edge(self.world_mut(), sub_graph, output_node, input_node);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn add_render_graph_edges<const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
edges: impl IntoRenderNodeArray<N>,
|
||||||
|
) -> &mut Self {
|
||||||
|
World::add_render_graph_edges(self.world_mut(), sub_graph, edges);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self {
|
||||||
|
World::add_render_sub_graph(self.world_mut(), sub_graph);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderGraphExt for App {
|
||||||
|
fn add_render_graph_node<T: Node + FromWorld>(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
node_label: impl RenderLabel,
|
||||||
|
) -> &mut Self {
|
||||||
|
World::add_render_graph_node::<T>(self.world_mut(), sub_graph, node_label);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_render_graph_edge(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
output_node: impl RenderLabel,
|
||||||
|
input_node: impl RenderLabel,
|
||||||
|
) -> &mut Self {
|
||||||
|
World::add_render_graph_edge(self.world_mut(), sub_graph, output_node, input_node);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_render_graph_edges<const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
sub_graph: impl RenderSubGraph,
|
||||||
|
edges: impl IntoRenderNodeArray<N>,
|
||||||
|
) -> &mut Self {
|
||||||
|
World::add_render_graph_edges(self.world_mut(), sub_graph, edges);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self {
|
||||||
|
World::add_render_sub_graph(self.world_mut(), sub_graph);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
107
third_party/bevy_render/src/render_graph/camera_driver_node.rs
vendored
Normal file
107
third_party/bevy_render/src/render_graph/camera_driver_node.rs
vendored
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
use crate::{
|
||||||
|
camera::{ExtractedCamera, SortedCameras},
|
||||||
|
render_graph::{Node, NodeRunError, RenderGraphContext},
|
||||||
|
renderer::RenderContext,
|
||||||
|
view::ExtractedWindows,
|
||||||
|
};
|
||||||
|
use bevy_camera::{ClearColor, NormalizedRenderTarget};
|
||||||
|
use bevy_ecs::{entity::ContainsEntity, prelude::QueryState, world::World};
|
||||||
|
use bevy_platform::collections::HashSet;
|
||||||
|
use wgpu::{LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp};
|
||||||
|
|
||||||
|
pub struct CameraDriverNode {
|
||||||
|
cameras: QueryState<&'static ExtractedCamera>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CameraDriverNode {
|
||||||
|
pub fn new(world: &mut World) -> Self {
|
||||||
|
Self {
|
||||||
|
cameras: world.query(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for CameraDriverNode {
|
||||||
|
fn update(&mut self, world: &mut World) {
|
||||||
|
self.cameras.update_archetypes(world);
|
||||||
|
}
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
graph: &mut RenderGraphContext,
|
||||||
|
render_context: &mut RenderContext,
|
||||||
|
world: &World,
|
||||||
|
) -> Result<(), NodeRunError> {
|
||||||
|
let sorted_cameras = world.resource::<SortedCameras>();
|
||||||
|
let windows = world.resource::<ExtractedWindows>();
|
||||||
|
let mut camera_windows = <HashSet<_>>::default();
|
||||||
|
for sorted_camera in &sorted_cameras.0 {
|
||||||
|
let Ok(camera) = self.cameras.get_manual(world, sorted_camera.entity) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut run_graph = true;
|
||||||
|
if let Some(NormalizedRenderTarget::Window(window_ref)) = camera.target {
|
||||||
|
let window_entity = window_ref.entity();
|
||||||
|
if windows
|
||||||
|
.windows
|
||||||
|
.get(&window_entity)
|
||||||
|
.is_some_and(|w| w.physical_width > 0 && w.physical_height > 0)
|
||||||
|
{
|
||||||
|
camera_windows.insert(window_entity);
|
||||||
|
} else {
|
||||||
|
// The window doesn't exist anymore or zero-sized so we don't need to run the graph
|
||||||
|
run_graph = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if run_graph {
|
||||||
|
graph.run_sub_graph(
|
||||||
|
camera.render_graph,
|
||||||
|
vec![],
|
||||||
|
Some(sorted_camera.entity),
|
||||||
|
Some(format!(
|
||||||
|
"Camera {} ({})",
|
||||||
|
sorted_camera.order, sorted_camera.entity
|
||||||
|
)),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let clear_color_global = world.resource::<ClearColor>();
|
||||||
|
|
||||||
|
// wgpu (and some backends) require doing work for swap chains if you call `get_current_texture()` and `present()`
|
||||||
|
// This ensures that Bevy doesn't crash, even when there are no cameras (and therefore no work submitted).
|
||||||
|
for (id, window) in world.resource::<ExtractedWindows>().iter() {
|
||||||
|
if camera_windows.contains(id) && render_context.has_commands() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(swap_chain_texture) = &window.swap_chain_texture_view else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "trace")]
|
||||||
|
let _span = tracing::info_span!("no_camera_clear_pass").entered();
|
||||||
|
let pass_descriptor = RenderPassDescriptor {
|
||||||
|
label: Some("no_camera_clear_pass"),
|
||||||
|
color_attachments: &[Some(RenderPassColorAttachment {
|
||||||
|
view: swap_chain_texture,
|
||||||
|
depth_slice: None,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: Operations {
|
||||||
|
load: LoadOp::Clear(clear_color_global.to_linear().into()),
|
||||||
|
store: StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
render_context
|
||||||
|
.command_encoder()
|
||||||
|
.begin_render_pass(&pass_descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
286
third_party/bevy_render/src/render_graph/context.rs
vendored
Normal file
286
third_party/bevy_render/src/render_graph/context.rs
vendored
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
use crate::{
|
||||||
|
render_graph::{NodeState, RenderGraph, SlotInfos, SlotLabel, SlotType, SlotValue},
|
||||||
|
render_resource::{Buffer, Sampler, TextureView},
|
||||||
|
};
|
||||||
|
use alloc::borrow::Cow;
|
||||||
|
use bevy_ecs::{entity::Entity, intern::Interned};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::{InternedRenderSubGraph, RenderLabel, RenderSubGraph};
|
||||||
|
|
||||||
|
/// A command that signals the graph runner to run the sub graph corresponding to the `sub_graph`
|
||||||
|
/// with the specified `inputs` next.
|
||||||
|
pub struct RunSubGraph {
|
||||||
|
pub sub_graph: InternedRenderSubGraph,
|
||||||
|
pub inputs: Vec<SlotValue>,
|
||||||
|
pub view_entity: Option<Entity>,
|
||||||
|
pub debug_group: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The context with all graph information required to run a [`Node`](super::Node).
|
||||||
|
/// This context is created for each node by the render graph runner.
|
||||||
|
///
|
||||||
|
/// The slot input can be read from here and the outputs must be written back to the context for
|
||||||
|
/// passing them onto the next node.
|
||||||
|
///
|
||||||
|
/// Sub graphs can be queued for running by adding a [`RunSubGraph`] command to the context.
|
||||||
|
/// After the node has finished running the graph runner is responsible for executing the sub graphs.
|
||||||
|
pub struct RenderGraphContext<'a> {
|
||||||
|
graph: &'a RenderGraph,
|
||||||
|
node: &'a NodeState,
|
||||||
|
inputs: &'a [SlotValue],
|
||||||
|
outputs: &'a mut [Option<SlotValue>],
|
||||||
|
run_sub_graphs: Vec<RunSubGraph>,
|
||||||
|
/// The `view_entity` associated with the render graph being executed
|
||||||
|
/// This is optional because you aren't required to have a `view_entity` for a node.
|
||||||
|
/// For example, compute shader nodes don't have one.
|
||||||
|
/// It should always be set when the [`RenderGraph`] is running on a View.
|
||||||
|
view_entity: Option<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RenderGraphContext<'a> {
|
||||||
|
/// Creates a new render graph context for the `node`.
|
||||||
|
pub fn new(
|
||||||
|
graph: &'a RenderGraph,
|
||||||
|
node: &'a NodeState,
|
||||||
|
inputs: &'a [SlotValue],
|
||||||
|
outputs: &'a mut [Option<SlotValue>],
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
graph,
|
||||||
|
node,
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
run_sub_graphs: Vec::new(),
|
||||||
|
view_entity: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the input slot values for the node.
|
||||||
|
#[inline]
|
||||||
|
pub fn inputs(&self) -> &[SlotValue] {
|
||||||
|
self.inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`SlotInfos`] of the inputs.
|
||||||
|
pub fn input_info(&self) -> &SlotInfos {
|
||||||
|
&self.node.input_slots
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`SlotInfos`] of the outputs.
|
||||||
|
pub fn output_info(&self) -> &SlotInfos {
|
||||||
|
&self.node.output_slots
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the input slot value referenced by the `label`.
|
||||||
|
pub fn get_input(&self, label: impl Into<SlotLabel>) -> Result<&SlotValue, InputSlotError> {
|
||||||
|
let label = label.into();
|
||||||
|
let index = self
|
||||||
|
.input_info()
|
||||||
|
.get_slot_index(label.clone())
|
||||||
|
.ok_or(InputSlotError::InvalidSlot(label))?;
|
||||||
|
Ok(&self.inputs[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: should this return an Arc or a reference?
|
||||||
|
/// Retrieves the input slot value referenced by the `label` as a [`TextureView`].
|
||||||
|
pub fn get_input_texture(
|
||||||
|
&self,
|
||||||
|
label: impl Into<SlotLabel>,
|
||||||
|
) -> Result<&TextureView, InputSlotError> {
|
||||||
|
let label = label.into();
|
||||||
|
match self.get_input(label.clone())? {
|
||||||
|
SlotValue::TextureView(value) => Ok(value),
|
||||||
|
value => Err(InputSlotError::MismatchedSlotType {
|
||||||
|
label,
|
||||||
|
actual: value.slot_type(),
|
||||||
|
expected: SlotType::TextureView,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the input slot value referenced by the `label` as a [`Sampler`].
|
||||||
|
pub fn get_input_sampler(
|
||||||
|
&self,
|
||||||
|
label: impl Into<SlotLabel>,
|
||||||
|
) -> Result<&Sampler, InputSlotError> {
|
||||||
|
let label = label.into();
|
||||||
|
match self.get_input(label.clone())? {
|
||||||
|
SlotValue::Sampler(value) => Ok(value),
|
||||||
|
value => Err(InputSlotError::MismatchedSlotType {
|
||||||
|
label,
|
||||||
|
actual: value.slot_type(),
|
||||||
|
expected: SlotType::Sampler,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the input slot value referenced by the `label` as a [`Buffer`].
|
||||||
|
pub fn get_input_buffer(&self, label: impl Into<SlotLabel>) -> Result<&Buffer, InputSlotError> {
|
||||||
|
let label = label.into();
|
||||||
|
match self.get_input(label.clone())? {
|
||||||
|
SlotValue::Buffer(value) => Ok(value),
|
||||||
|
value => Err(InputSlotError::MismatchedSlotType {
|
||||||
|
label,
|
||||||
|
actual: value.slot_type(),
|
||||||
|
expected: SlotType::Buffer,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the input slot value referenced by the `label` as an [`Entity`].
|
||||||
|
pub fn get_input_entity(&self, label: impl Into<SlotLabel>) -> Result<Entity, InputSlotError> {
|
||||||
|
let label = label.into();
|
||||||
|
match self.get_input(label.clone())? {
|
||||||
|
SlotValue::Entity(value) => Ok(*value),
|
||||||
|
value => Err(InputSlotError::MismatchedSlotType {
|
||||||
|
label,
|
||||||
|
actual: value.slot_type(),
|
||||||
|
expected: SlotType::Entity,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the output slot value referenced by the `label`.
|
||||||
|
pub fn set_output(
|
||||||
|
&mut self,
|
||||||
|
label: impl Into<SlotLabel>,
|
||||||
|
value: impl Into<SlotValue>,
|
||||||
|
) -> Result<(), OutputSlotError> {
|
||||||
|
let label = label.into();
|
||||||
|
let value = value.into();
|
||||||
|
let slot_index = self
|
||||||
|
.output_info()
|
||||||
|
.get_slot_index(label.clone())
|
||||||
|
.ok_or_else(|| OutputSlotError::InvalidSlot(label.clone()))?;
|
||||||
|
let slot = self
|
||||||
|
.output_info()
|
||||||
|
.get_slot(slot_index)
|
||||||
|
.expect("slot is valid");
|
||||||
|
if value.slot_type() != slot.slot_type {
|
||||||
|
return Err(OutputSlotError::MismatchedSlotType {
|
||||||
|
label,
|
||||||
|
actual: slot.slot_type,
|
||||||
|
expected: value.slot_type(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.outputs[slot_index] = Some(value);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_entity(&self) -> Entity {
|
||||||
|
self.view_entity.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_view_entity(&self) -> Option<Entity> {
|
||||||
|
self.view_entity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_view_entity(&mut self, view_entity: Entity) {
|
||||||
|
self.view_entity = Some(view_entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queues up a sub graph for execution after the node has finished running.
|
||||||
|
pub fn run_sub_graph(
|
||||||
|
&mut self,
|
||||||
|
name: impl RenderSubGraph,
|
||||||
|
inputs: Vec<SlotValue>,
|
||||||
|
view_entity: Option<Entity>,
|
||||||
|
debug_group: Option<String>,
|
||||||
|
) -> Result<(), RunSubGraphError> {
|
||||||
|
let name = name.intern();
|
||||||
|
let sub_graph = self
|
||||||
|
.graph
|
||||||
|
.get_sub_graph(name)
|
||||||
|
.ok_or(RunSubGraphError::MissingSubGraph(name))?;
|
||||||
|
if let Some(input_node) = sub_graph.get_input_node() {
|
||||||
|
for (i, input_slot) in input_node.input_slots.iter().enumerate() {
|
||||||
|
if let Some(input_value) = inputs.get(i) {
|
||||||
|
if input_slot.slot_type != input_value.slot_type() {
|
||||||
|
return Err(RunSubGraphError::MismatchedInputSlotType {
|
||||||
|
graph_name: name,
|
||||||
|
slot_index: i,
|
||||||
|
actual: input_value.slot_type(),
|
||||||
|
expected: input_slot.slot_type,
|
||||||
|
label: input_slot.name.clone().into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(RunSubGraphError::MissingInput {
|
||||||
|
slot_index: i,
|
||||||
|
slot_name: input_slot.name.clone(),
|
||||||
|
graph_name: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !inputs.is_empty() {
|
||||||
|
return Err(RunSubGraphError::SubGraphHasNoInputs(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.run_sub_graphs.push(RunSubGraph {
|
||||||
|
sub_graph: name,
|
||||||
|
inputs,
|
||||||
|
view_entity,
|
||||||
|
debug_group,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a human-readable label for this node, for debugging purposes.
|
||||||
|
pub fn label(&self) -> Interned<dyn RenderLabel> {
|
||||||
|
self.node.label
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finishes the context for this [`Node`](super::Node) by
|
||||||
|
/// returning the sub graphs to run next.
|
||||||
|
pub fn finish(self) -> Vec<RunSubGraph> {
|
||||||
|
self.run_sub_graphs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RunSubGraphError {
|
||||||
|
#[error("attempted to run sub-graph `{0:?}`, but it does not exist")]
|
||||||
|
MissingSubGraph(InternedRenderSubGraph),
|
||||||
|
#[error("attempted to pass inputs to sub-graph `{0:?}`, which has no input slots")]
|
||||||
|
SubGraphHasNoInputs(InternedRenderSubGraph),
|
||||||
|
#[error("sub graph (name: `{graph_name:?}`) could not be run because slot `{slot_name}` at index {slot_index} has no value")]
|
||||||
|
MissingInput {
|
||||||
|
slot_index: usize,
|
||||||
|
slot_name: Cow<'static, str>,
|
||||||
|
graph_name: InternedRenderSubGraph,
|
||||||
|
},
|
||||||
|
#[error("attempted to use the wrong type for input slot")]
|
||||||
|
MismatchedInputSlotType {
|
||||||
|
graph_name: InternedRenderSubGraph,
|
||||||
|
slot_index: usize,
|
||||||
|
label: SlotLabel,
|
||||||
|
expected: SlotType,
|
||||||
|
actual: SlotType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Eq, PartialEq)]
|
||||||
|
pub enum OutputSlotError {
|
||||||
|
#[error("output slot `{0:?}` does not exist")]
|
||||||
|
InvalidSlot(SlotLabel),
|
||||||
|
#[error("attempted to output a value of type `{actual}` to output slot `{label:?}`, which has type `{expected}`")]
|
||||||
|
MismatchedSlotType {
|
||||||
|
label: SlotLabel,
|
||||||
|
expected: SlotType,
|
||||||
|
actual: SlotType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Eq, PartialEq)]
|
||||||
|
pub enum InputSlotError {
|
||||||
|
#[error("input slot `{0:?}` does not exist")]
|
||||||
|
InvalidSlot(SlotLabel),
|
||||||
|
#[error("attempted to retrieve a value of type `{actual}` from input slot `{label:?}`, which has type `{expected}`")]
|
||||||
|
MismatchedSlotType {
|
||||||
|
label: SlotLabel,
|
||||||
|
expected: SlotType,
|
||||||
|
actual: SlotType,
|
||||||
|
},
|
||||||
|
}
|
||||||
57
third_party/bevy_render/src/render_graph/edge.rs
vendored
Normal file
57
third_party/bevy_render/src/render_graph/edge.rs
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use super::InternedRenderLabel;
|
||||||
|
|
||||||
|
/// An edge, which connects two [`Nodes`](super::Node) in
|
||||||
|
/// a [`RenderGraph`](crate::render_graph::RenderGraph).
|
||||||
|
///
|
||||||
|
/// They are used to describe the ordering (which node has to run first)
|
||||||
|
/// and may be of two kinds: [`NodeEdge`](Self::NodeEdge) and [`SlotEdge`](Self::SlotEdge).
|
||||||
|
///
|
||||||
|
/// Edges are added via the [`RenderGraph::add_node_edge`] and the
|
||||||
|
/// [`RenderGraph::add_slot_edge`] methods.
|
||||||
|
///
|
||||||
|
/// The former simply states that the `output_node` has to be run before the `input_node`,
|
||||||
|
/// while the later connects an output slot of the `output_node`
|
||||||
|
/// with an input slot of the `input_node` to pass additional data along.
|
||||||
|
/// For more information see [`SlotType`](super::SlotType).
|
||||||
|
///
|
||||||
|
/// [`RenderGraph::add_node_edge`]: crate::render_graph::RenderGraph::add_node_edge
|
||||||
|
/// [`RenderGraph::add_slot_edge`]: crate::render_graph::RenderGraph::add_slot_edge
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Edge {
|
||||||
|
/// An edge describing to ordering of both nodes (`output_node` before `input_node`)
|
||||||
|
/// and connecting the output slot at the `output_index` of the `output_node`
|
||||||
|
/// with the slot at the `input_index` of the `input_node`.
|
||||||
|
SlotEdge {
|
||||||
|
input_node: InternedRenderLabel,
|
||||||
|
input_index: usize,
|
||||||
|
output_node: InternedRenderLabel,
|
||||||
|
output_index: usize,
|
||||||
|
},
|
||||||
|
/// An edge describing to ordering of both nodes (`output_node` before `input_node`).
|
||||||
|
NodeEdge {
|
||||||
|
input_node: InternedRenderLabel,
|
||||||
|
output_node: InternedRenderLabel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Edge {
|
||||||
|
/// Returns the id of the `input_node`.
|
||||||
|
pub fn get_input_node(&self) -> InternedRenderLabel {
|
||||||
|
match self {
|
||||||
|
Edge::SlotEdge { input_node, .. } | Edge::NodeEdge { input_node, .. } => *input_node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the id of the `output_node`.
|
||||||
|
pub fn get_output_node(&self) -> InternedRenderLabel {
|
||||||
|
match self {
|
||||||
|
Edge::SlotEdge { output_node, .. } | Edge::NodeEdge { output_node, .. } => *output_node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub enum EdgeExistence {
|
||||||
|
Exists,
|
||||||
|
DoesNotExist,
|
||||||
|
}
|
||||||
918
third_party/bevy_render/src/render_graph/graph.rs
vendored
Normal file
918
third_party/bevy_render/src/render_graph/graph.rs
vendored
Normal file
@ -0,0 +1,918 @@
|
|||||||
|
use crate::{
|
||||||
|
render_graph::{
|
||||||
|
Edge, Node, NodeRunError, NodeState, RenderGraphContext, RenderGraphError, RenderLabel,
|
||||||
|
SlotInfo, SlotLabel,
|
||||||
|
},
|
||||||
|
renderer::RenderContext,
|
||||||
|
};
|
||||||
|
use bevy_ecs::{define_label, intern::Interned, prelude::World, resource::Resource};
|
||||||
|
use bevy_platform::collections::HashMap;
|
||||||
|
use core::fmt::Debug;
|
||||||
|
|
||||||
|
use super::{EdgeExistence, InternedRenderLabel, IntoRenderNodeArray};
|
||||||
|
|
||||||
|
pub use bevy_render_macros::RenderSubGraph;
|
||||||
|
|
||||||
|
define_label!(
|
||||||
|
#[diagnostic::on_unimplemented(
|
||||||
|
note = "consider annotating `{Self}` with `#[derive(RenderSubGraph)]`"
|
||||||
|
)]
|
||||||
|
/// A strongly-typed class of labels used to identify a [`SubGraph`] in a render graph.
|
||||||
|
RenderSubGraph,
|
||||||
|
RENDER_SUB_GRAPH_INTERNER
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A shorthand for `Interned<dyn RenderSubGraph>`.
|
||||||
|
pub type InternedRenderSubGraph = Interned<dyn RenderSubGraph>;
|
||||||
|
|
||||||
|
/// The render graph configures the modular and re-usable render logic.
|
||||||
|
///
|
||||||
|
/// It is a retained and stateless (nodes themselves may have their own internal state) structure,
|
||||||
|
/// which can not be modified while it is executed by the graph runner.
|
||||||
|
///
|
||||||
|
/// The render graph runner is responsible for executing the entire graph each frame.
|
||||||
|
/// It will execute each node in the graph in the correct order, based on the edges between the nodes.
|
||||||
|
///
|
||||||
|
/// It consists of three main components: [`Nodes`](Node), [`Edges`](Edge)
|
||||||
|
/// and [`Slots`](super::SlotType).
|
||||||
|
///
|
||||||
|
/// Nodes are responsible for generating draw calls and operating on input and output slots.
|
||||||
|
/// Edges specify the order of execution for nodes and connect input and output slots together.
|
||||||
|
/// Slots describe the render resources created or used by the nodes.
|
||||||
|
///
|
||||||
|
/// Additionally a render graph can contain multiple sub graphs, which are run by the
|
||||||
|
/// corresponding nodes. Every render graph can have its own optional input node.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// Here is a simple render graph example with two nodes connected by a node edge.
|
||||||
|
/// ```ignore
|
||||||
|
/// # TODO: Remove when #10645 is fixed
|
||||||
|
/// # use bevy_app::prelude::*;
|
||||||
|
/// # use bevy_ecs::prelude::World;
|
||||||
|
/// # use bevy_render::render_graph::{RenderGraph, RenderLabel, Node, RenderGraphContext, NodeRunError};
|
||||||
|
/// # use bevy_render::renderer::RenderContext;
|
||||||
|
/// #
|
||||||
|
/// #[derive(RenderLabel)]
|
||||||
|
/// enum Labels {
|
||||||
|
/// A,
|
||||||
|
/// B,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// # struct MyNode;
|
||||||
|
/// #
|
||||||
|
/// # impl Node for MyNode {
|
||||||
|
/// # fn run(&self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, world: &World) -> Result<(), NodeRunError> {
|
||||||
|
/// # unimplemented!()
|
||||||
|
/// # }
|
||||||
|
/// # }
|
||||||
|
/// #
|
||||||
|
/// let mut graph = RenderGraph::default();
|
||||||
|
/// graph.add_node(Labels::A, MyNode);
|
||||||
|
/// graph.add_node(Labels::B, MyNode);
|
||||||
|
/// graph.add_node_edge(Labels::B, Labels::A);
|
||||||
|
/// ```
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct RenderGraph {
|
||||||
|
nodes: HashMap<InternedRenderLabel, NodeState>,
|
||||||
|
sub_graphs: HashMap<InternedRenderSubGraph, RenderGraph>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The label for the input node of a graph. Used to connect other nodes to it.
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
|
||||||
|
pub struct GraphInput;
|
||||||
|
|
||||||
|
impl RenderGraph {
|
||||||
|
/// Updates all nodes and sub graphs of the render graph. Should be called before executing it.
|
||||||
|
pub fn update(&mut self, world: &mut World) {
|
||||||
|
for node in self.nodes.values_mut() {
|
||||||
|
node.node.update(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
for sub_graph in self.sub_graphs.values_mut() {
|
||||||
|
sub_graph.update(world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an [`GraphInputNode`] with the specified slots if not already present.
|
||||||
|
pub fn set_input(&mut self, inputs: Vec<SlotInfo>) {
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
self.get_node_state(GraphInput),
|
||||||
|
Err(RenderGraphError::InvalidNode(_))
|
||||||
|
),
|
||||||
|
"Graph already has an input node"
|
||||||
|
);
|
||||||
|
|
||||||
|
self.add_node(GraphInput, GraphInputNode { inputs });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`NodeState`] of the input node of this graph.
|
||||||
|
///
|
||||||
|
/// # See also
|
||||||
|
///
|
||||||
|
/// - [`input_node`](Self::input_node) for an unchecked version.
|
||||||
|
#[inline]
|
||||||
|
pub fn get_input_node(&self) -> Option<&NodeState> {
|
||||||
|
self.get_node_state(GraphInput).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [`NodeState`] of the input node of this graph.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if there is no input node set.
|
||||||
|
///
|
||||||
|
/// # See also
|
||||||
|
///
|
||||||
|
/// - [`get_input_node`](Self::get_input_node) for a version which returns an [`Option`] instead.
|
||||||
|
#[inline]
|
||||||
|
pub fn input_node(&self) -> &NodeState {
|
||||||
|
self.get_input_node().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the `node` with the `label` to the graph.
|
||||||
|
/// If the label is already present replaces it instead.
|
||||||
|
pub fn add_node<T>(&mut self, label: impl RenderLabel, node: T)
|
||||||
|
where
|
||||||
|
T: Node,
|
||||||
|
{
|
||||||
|
let label = label.intern();
|
||||||
|
let node_state = NodeState::new(label, node);
|
||||||
|
self.nodes.insert(label, node_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add `node_edge`s based on the order of the given `edges` array.
|
||||||
|
///
|
||||||
|
/// Defining an edge that already exists is not considered an error with this api.
|
||||||
|
/// It simply won't create a new edge.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn add_node_edges<const N: usize>(&mut self, edges: impl IntoRenderNodeArray<N>) {
|
||||||
|
for window in edges.into_array().windows(2) {
|
||||||
|
let [a, b] = window else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if let Err(err) = self.try_add_node_edge(*a, *b) {
|
||||||
|
match err {
|
||||||
|
// Already existing edges are very easy to produce with this api
|
||||||
|
// and shouldn't cause a panic
|
||||||
|
RenderGraphError::EdgeAlreadyExists(_) => {}
|
||||||
|
_ => panic!("{err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the `node` with the `label` from the graph.
|
||||||
|
/// If the label does not exist, nothing happens.
|
||||||
|
pub fn remove_node(&mut self, label: impl RenderLabel) -> Result<(), RenderGraphError> {
|
||||||
|
let label = label.intern();
|
||||||
|
if let Some(node_state) = self.nodes.remove(&label) {
|
||||||
|
// Remove all edges from other nodes to this one. Note that as we're removing this
|
||||||
|
// node, we don't need to remove its input edges
|
||||||
|
for input_edge in node_state.edges.input_edges() {
|
||||||
|
match input_edge {
|
||||||
|
Edge::SlotEdge { output_node, .. }
|
||||||
|
| Edge::NodeEdge {
|
||||||
|
input_node: _,
|
||||||
|
output_node,
|
||||||
|
} => {
|
||||||
|
if let Ok(output_node) = self.get_node_state_mut(*output_node) {
|
||||||
|
output_node.edges.remove_output_edge(input_edge.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove all edges from this node to other nodes. Note that as we're removing this
|
||||||
|
// node, we don't need to remove its output edges
|
||||||
|
for output_edge in node_state.edges.output_edges() {
|
||||||
|
match output_edge {
|
||||||
|
Edge::SlotEdge {
|
||||||
|
output_node: _,
|
||||||
|
output_index: _,
|
||||||
|
input_node,
|
||||||
|
input_index: _,
|
||||||
|
}
|
||||||
|
| Edge::NodeEdge {
|
||||||
|
output_node: _,
|
||||||
|
input_node,
|
||||||
|
} => {
|
||||||
|
if let Ok(input_node) = self.get_node_state_mut(*input_node) {
|
||||||
|
input_node.edges.remove_input_edge(output_edge.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [`NodeState`] referenced by the `label`.
|
||||||
|
pub fn get_node_state(&self, label: impl RenderLabel) -> Result<&NodeState, RenderGraphError> {
|
||||||
|
let label = label.intern();
|
||||||
|
self.nodes
|
||||||
|
.get(&label)
|
||||||
|
.ok_or(RenderGraphError::InvalidNode(label))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [`NodeState`] referenced by the `label` mutably.
|
||||||
|
pub fn get_node_state_mut(
|
||||||
|
&mut self,
|
||||||
|
label: impl RenderLabel,
|
||||||
|
) -> Result<&mut NodeState, RenderGraphError> {
|
||||||
|
let label = label.intern();
|
||||||
|
self.nodes
|
||||||
|
.get_mut(&label)
|
||||||
|
.ok_or(RenderGraphError::InvalidNode(label))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [`Node`] referenced by the `label`.
|
||||||
|
pub fn get_node<T>(&self, label: impl RenderLabel) -> Result<&T, RenderGraphError>
|
||||||
|
where
|
||||||
|
T: Node,
|
||||||
|
{
|
||||||
|
self.get_node_state(label).and_then(|n| n.node())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [`Node`] referenced by the `label` mutably.
|
||||||
|
pub fn get_node_mut<T>(&mut self, label: impl RenderLabel) -> Result<&mut T, RenderGraphError>
|
||||||
|
where
|
||||||
|
T: Node,
|
||||||
|
{
|
||||||
|
self.get_node_state_mut(label).and_then(|n| n.node_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the [`Edge::SlotEdge`] to the graph. This guarantees that the `output_node`
|
||||||
|
/// is run before the `input_node` and also connects the `output_slot` to the `input_slot`.
|
||||||
|
///
|
||||||
|
/// Fails if any invalid [`RenderLabel`]s or [`SlotLabel`]s are given.
|
||||||
|
///
|
||||||
|
/// # See also
|
||||||
|
///
|
||||||
|
/// - [`add_slot_edge`](Self::add_slot_edge) for an infallible version.
|
||||||
|
pub fn try_add_slot_edge(
|
||||||
|
&mut self,
|
||||||
|
output_node: impl RenderLabel,
|
||||||
|
output_slot: impl Into<SlotLabel>,
|
||||||
|
input_node: impl RenderLabel,
|
||||||
|
input_slot: impl Into<SlotLabel>,
|
||||||
|
) -> Result<(), RenderGraphError> {
|
||||||
|
let output_slot = output_slot.into();
|
||||||
|
let input_slot = input_slot.into();
|
||||||
|
|
||||||
|
let output_node = output_node.intern();
|
||||||
|
let input_node = input_node.intern();
|
||||||
|
|
||||||
|
let output_index = self
|
||||||
|
.get_node_state(output_node)?
|
||||||
|
.output_slots
|
||||||
|
.get_slot_index(output_slot.clone())
|
||||||
|
.ok_or(RenderGraphError::InvalidOutputNodeSlot(output_slot))?;
|
||||||
|
let input_index = self
|
||||||
|
.get_node_state(input_node)?
|
||||||
|
.input_slots
|
||||||
|
.get_slot_index(input_slot.clone())
|
||||||
|
.ok_or(RenderGraphError::InvalidInputNodeSlot(input_slot))?;
|
||||||
|
|
||||||
|
let edge = Edge::SlotEdge {
|
||||||
|
output_node,
|
||||||
|
output_index,
|
||||||
|
input_node,
|
||||||
|
input_index,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.validate_edge(&edge, EdgeExistence::DoesNotExist)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let output_node = self.get_node_state_mut(output_node)?;
|
||||||
|
output_node.edges.add_output_edge(edge.clone())?;
|
||||||
|
}
|
||||||
|
let input_node = self.get_node_state_mut(input_node)?;
|
||||||
|
input_node.edges.add_input_edge(edge)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the [`Edge::SlotEdge`] to the graph. This guarantees that the `output_node`
|
||||||
|
/// is run before the `input_node` and also connects the `output_slot` to the `input_slot`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Any invalid [`RenderLabel`]s or [`SlotLabel`]s are given.
|
||||||
|
///
|
||||||
|
/// # See also
|
||||||
|
///
|
||||||
|
/// - [`try_add_slot_edge`](Self::try_add_slot_edge) for a fallible version.
|
||||||
|
pub fn add_slot_edge(
|
||||||
|
&mut self,
|
||||||
|
output_node: impl RenderLabel,
|
||||||
|
output_slot: impl Into<SlotLabel>,
|
||||||
|
input_node: impl RenderLabel,
|
||||||
|
input_slot: impl Into<SlotLabel>,
|
||||||
|
) {
|
||||||
|
self.try_add_slot_edge(output_node, output_slot, input_node, input_slot)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the [`Edge::SlotEdge`] from the graph. If any nodes or slots do not exist then
|
||||||
|
/// nothing happens.
|
||||||
|
pub fn remove_slot_edge(
|
||||||
|
&mut self,
|
||||||
|
output_node: impl RenderLabel,
|
||||||
|
output_slot: impl Into<SlotLabel>,
|
||||||
|
input_node: impl RenderLabel,
|
||||||
|
input_slot: impl Into<SlotLabel>,
|
||||||
|
) -> Result<(), RenderGraphError> {
|
||||||
|
let output_slot = output_slot.into();
|
||||||
|
let input_slot = input_slot.into();
|
||||||
|
|
||||||
|
let output_node = output_node.intern();
|
||||||
|
let input_node = input_node.intern();
|
||||||
|
|
||||||
|
let output_index = self
|
||||||
|
.get_node_state(output_node)?
|
||||||
|
.output_slots
|
||||||
|
.get_slot_index(output_slot.clone())
|
||||||
|
.ok_or(RenderGraphError::InvalidOutputNodeSlot(output_slot))?;
|
||||||
|
let input_index = self
|
||||||
|
.get_node_state(input_node)?
|
||||||
|
.input_slots
|
||||||
|
.get_slot_index(input_slot.clone())
|
||||||
|
.ok_or(RenderGraphError::InvalidInputNodeSlot(input_slot))?;
|
||||||
|
|
||||||
|
let edge = Edge::SlotEdge {
|
||||||
|
output_node,
|
||||||
|
output_index,
|
||||||
|
input_node,
|
||||||
|
input_index,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.validate_edge(&edge, EdgeExistence::Exists)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let output_node = self.get_node_state_mut(output_node)?;
|
||||||
|
output_node.edges.remove_output_edge(edge.clone())?;
|
||||||
|
}
|
||||||
|
let input_node = self.get_node_state_mut(input_node)?;
|
||||||
|
input_node.edges.remove_input_edge(edge)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the [`Edge::NodeEdge`] to the graph. This guarantees that the `output_node`
|
||||||
|
/// is run before the `input_node`.
|
||||||
|
///
|
||||||
|
/// Fails if any invalid [`RenderLabel`] is given.
|
||||||
|
///
|
||||||
|
/// # See also
|
||||||
|
///
|
||||||
|
/// - [`add_node_edge`](Self::add_node_edge) for an infallible version.
|
||||||
|
pub fn try_add_node_edge(
|
||||||
|
&mut self,
|
||||||
|
output_node: impl RenderLabel,
|
||||||
|
input_node: impl RenderLabel,
|
||||||
|
) -> Result<(), RenderGraphError> {
|
||||||
|
let output_node = output_node.intern();
|
||||||
|
let input_node = input_node.intern();
|
||||||
|
|
||||||
|
let edge = Edge::NodeEdge {
|
||||||
|
output_node,
|
||||||
|
input_node,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.validate_edge(&edge, EdgeExistence::DoesNotExist)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let output_node = self.get_node_state_mut(output_node)?;
|
||||||
|
output_node.edges.add_output_edge(edge.clone())?;
|
||||||
|
}
|
||||||
|
let input_node = self.get_node_state_mut(input_node)?;
|
||||||
|
input_node.edges.add_input_edge(edge)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the [`Edge::NodeEdge`] to the graph. This guarantees that the `output_node`
|
||||||
|
/// is run before the `input_node`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if any invalid [`RenderLabel`] is given.
|
||||||
|
///
|
||||||
|
/// # See also
|
||||||
|
///
|
||||||
|
/// - [`try_add_node_edge`](Self::try_add_node_edge) for a fallible version.
|
||||||
|
pub fn add_node_edge(&mut self, output_node: impl RenderLabel, input_node: impl RenderLabel) {
|
||||||
|
self.try_add_node_edge(output_node, input_node).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the [`Edge::NodeEdge`] from the graph. If either node does not exist then nothing
|
||||||
|
/// happens.
|
||||||
|
pub fn remove_node_edge(
|
||||||
|
&mut self,
|
||||||
|
output_node: impl RenderLabel,
|
||||||
|
input_node: impl RenderLabel,
|
||||||
|
) -> Result<(), RenderGraphError> {
|
||||||
|
let output_node = output_node.intern();
|
||||||
|
let input_node = input_node.intern();
|
||||||
|
|
||||||
|
let edge = Edge::NodeEdge {
|
||||||
|
output_node,
|
||||||
|
input_node,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.validate_edge(&edge, EdgeExistence::Exists)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let output_node = self.get_node_state_mut(output_node)?;
|
||||||
|
output_node.edges.remove_output_edge(edge.clone())?;
|
||||||
|
}
|
||||||
|
let input_node = self.get_node_state_mut(input_node)?;
|
||||||
|
input_node.edges.remove_input_edge(edge)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies that the edge existence is as expected and
|
||||||
|
/// checks that slot edges are connected correctly.
|
||||||
|
pub fn validate_edge(
|
||||||
|
&mut self,
|
||||||
|
edge: &Edge,
|
||||||
|
should_exist: EdgeExistence,
|
||||||
|
) -> Result<(), RenderGraphError> {
|
||||||
|
if should_exist == EdgeExistence::Exists && !self.has_edge(edge) {
|
||||||
|
return Err(RenderGraphError::EdgeDoesNotExist(edge.clone()));
|
||||||
|
} else if should_exist == EdgeExistence::DoesNotExist && self.has_edge(edge) {
|
||||||
|
return Err(RenderGraphError::EdgeAlreadyExists(edge.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
match *edge {
|
||||||
|
Edge::SlotEdge {
|
||||||
|
output_node,
|
||||||
|
output_index,
|
||||||
|
input_node,
|
||||||
|
input_index,
|
||||||
|
} => {
|
||||||
|
let output_node_state = self.get_node_state(output_node)?;
|
||||||
|
let input_node_state = self.get_node_state(input_node)?;
|
||||||
|
|
||||||
|
let output_slot = output_node_state
|
||||||
|
.output_slots
|
||||||
|
.get_slot(output_index)
|
||||||
|
.ok_or(RenderGraphError::InvalidOutputNodeSlot(SlotLabel::Index(
|
||||||
|
output_index,
|
||||||
|
)))?;
|
||||||
|
let input_slot = input_node_state.input_slots.get_slot(input_index).ok_or(
|
||||||
|
RenderGraphError::InvalidInputNodeSlot(SlotLabel::Index(input_index)),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if let Some(Edge::SlotEdge {
|
||||||
|
output_node: current_output_node,
|
||||||
|
..
|
||||||
|
}) = input_node_state.edges.input_edges().iter().find(|e| {
|
||||||
|
if let Edge::SlotEdge {
|
||||||
|
input_index: current_input_index,
|
||||||
|
..
|
||||||
|
} = e
|
||||||
|
{
|
||||||
|
input_index == *current_input_index
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}) && should_exist == EdgeExistence::DoesNotExist
|
||||||
|
{
|
||||||
|
return Err(RenderGraphError::NodeInputSlotAlreadyOccupied {
|
||||||
|
node: input_node,
|
||||||
|
input_slot: input_index,
|
||||||
|
occupied_by_node: *current_output_node,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if output_slot.slot_type != input_slot.slot_type {
|
||||||
|
return Err(RenderGraphError::MismatchedNodeSlots {
|
||||||
|
output_node,
|
||||||
|
output_slot: output_index,
|
||||||
|
input_node,
|
||||||
|
input_slot: input_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Edge::NodeEdge { .. } => { /* nothing to validate here */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the `edge` already exists in the graph.
|
||||||
|
pub fn has_edge(&self, edge: &Edge) -> bool {
|
||||||
|
let output_node_state = self.get_node_state(edge.get_output_node());
|
||||||
|
let input_node_state = self.get_node_state(edge.get_input_node());
|
||||||
|
if let Ok(output_node_state) = output_node_state
|
||||||
|
&& output_node_state.edges.output_edges().contains(edge)
|
||||||
|
&& let Ok(input_node_state) = input_node_state
|
||||||
|
&& input_node_state.edges.input_edges().contains(edge)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the [`NodeStates`](NodeState).
|
||||||
|
pub fn iter_nodes(&self) -> impl Iterator<Item = &NodeState> {
|
||||||
|
self.nodes.values()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the [`NodeStates`](NodeState), that allows modifying each value.
|
||||||
|
pub fn iter_nodes_mut(&mut self) -> impl Iterator<Item = &mut NodeState> {
|
||||||
|
self.nodes.values_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the sub graphs.
|
||||||
|
pub fn iter_sub_graphs(&self) -> impl Iterator<Item = (InternedRenderSubGraph, &RenderGraph)> {
|
||||||
|
self.sub_graphs.iter().map(|(name, graph)| (*name, graph))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the sub graphs, that allows modifying each value.
|
||||||
|
pub fn iter_sub_graphs_mut(
|
||||||
|
&mut self,
|
||||||
|
) -> impl Iterator<Item = (InternedRenderSubGraph, &mut RenderGraph)> {
|
||||||
|
self.sub_graphs
|
||||||
|
.iter_mut()
|
||||||
|
.map(|(name, graph)| (*name, graph))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over a tuple of the input edges and the corresponding output nodes
|
||||||
|
/// for the node referenced by the label.
|
||||||
|
pub fn iter_node_inputs(
|
||||||
|
&self,
|
||||||
|
label: impl RenderLabel,
|
||||||
|
) -> Result<impl Iterator<Item = (&Edge, &NodeState)>, RenderGraphError> {
|
||||||
|
let node = self.get_node_state(label)?;
|
||||||
|
Ok(node
|
||||||
|
.edges
|
||||||
|
.input_edges()
|
||||||
|
.iter()
|
||||||
|
.map(|edge| (edge, edge.get_output_node()))
|
||||||
|
.map(move |(edge, output_node)| (edge, self.get_node_state(output_node).unwrap())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over a tuple of the output edges and the corresponding input nodes
|
||||||
|
/// for the node referenced by the label.
|
||||||
|
pub fn iter_node_outputs(
|
||||||
|
&self,
|
||||||
|
label: impl RenderLabel,
|
||||||
|
) -> Result<impl Iterator<Item = (&Edge, &NodeState)>, RenderGraphError> {
|
||||||
|
let node = self.get_node_state(label)?;
|
||||||
|
Ok(node
|
||||||
|
.edges
|
||||||
|
.output_edges()
|
||||||
|
.iter()
|
||||||
|
.map(|edge| (edge, edge.get_input_node()))
|
||||||
|
.map(move |(edge, input_node)| (edge, self.get_node_state(input_node).unwrap())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the `sub_graph` with the `label` to the graph.
|
||||||
|
/// If the label is already present replaces it instead.
|
||||||
|
pub fn add_sub_graph(&mut self, label: impl RenderSubGraph, sub_graph: RenderGraph) {
|
||||||
|
self.sub_graphs.insert(label.intern(), sub_graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the `sub_graph` with the `label` from the graph.
|
||||||
|
/// If the label does not exist then nothing happens.
|
||||||
|
pub fn remove_sub_graph(&mut self, label: impl RenderSubGraph) {
|
||||||
|
self.sub_graphs.remove(&label.intern());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the sub graph corresponding to the `label`.
|
||||||
|
pub fn get_sub_graph(&self, label: impl RenderSubGraph) -> Option<&RenderGraph> {
|
||||||
|
self.sub_graphs.get(&label.intern())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the sub graph corresponding to the `label` mutably.
|
||||||
|
pub fn get_sub_graph_mut(&mut self, label: impl RenderSubGraph) -> Option<&mut RenderGraph> {
|
||||||
|
self.sub_graphs.get_mut(&label.intern())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the sub graph corresponding to the `label`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if any invalid subgraph label is given.
|
||||||
|
///
|
||||||
|
/// # See also
|
||||||
|
///
|
||||||
|
/// - [`get_sub_graph`](Self::get_sub_graph) for a fallible version.
|
||||||
|
pub fn sub_graph(&self, label: impl RenderSubGraph) -> &RenderGraph {
|
||||||
|
let label = label.intern();
|
||||||
|
self.sub_graphs
|
||||||
|
.get(&label)
|
||||||
|
.unwrap_or_else(|| panic!("Subgraph {label:?} not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the sub graph corresponding to the `label` mutably.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if any invalid subgraph label is given.
|
||||||
|
///
|
||||||
|
/// # See also
|
||||||
|
///
|
||||||
|
/// - [`get_sub_graph_mut`](Self::get_sub_graph_mut) for a fallible version.
|
||||||
|
pub fn sub_graph_mut(&mut self, label: impl RenderSubGraph) -> &mut RenderGraph {
|
||||||
|
let label = label.intern();
|
||||||
|
self.sub_graphs
|
||||||
|
.get_mut(&label)
|
||||||
|
.unwrap_or_else(|| panic!("Subgraph {label:?} not found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for RenderGraph {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
for node in self.iter_nodes() {
|
||||||
|
writeln!(f, "{:?}", node.label)?;
|
||||||
|
writeln!(f, " in: {:?}", node.input_slots)?;
|
||||||
|
writeln!(f, " out: {:?}", node.output_slots)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Node`] which acts as an entry point for a [`RenderGraph`] with custom inputs.
|
||||||
|
/// It has the same input and output slots and simply copies them over when run.
|
||||||
|
pub struct GraphInputNode {
|
||||||
|
inputs: Vec<SlotInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for GraphInputNode {
|
||||||
|
fn input(&self) -> Vec<SlotInfo> {
|
||||||
|
self.inputs.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output(&self) -> Vec<SlotInfo> {
|
||||||
|
self.inputs.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
graph: &mut RenderGraphContext,
|
||||||
|
_render_context: &mut RenderContext,
|
||||||
|
_world: &World,
|
||||||
|
) -> Result<(), NodeRunError> {
|
||||||
|
for i in 0..graph.inputs().len() {
|
||||||
|
let input = graph.inputs()[i].clone();
|
||||||
|
graph.set_output(i, input)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
render_graph::{
|
||||||
|
node::IntoRenderNodeArray, Edge, InternedRenderLabel, Node, NodeRunError, RenderGraph,
|
||||||
|
RenderGraphContext, RenderGraphError, RenderLabel, SlotInfo, SlotType,
|
||||||
|
},
|
||||||
|
renderer::RenderContext,
|
||||||
|
};
|
||||||
|
use bevy_ecs::world::{FromWorld, World};
|
||||||
|
use bevy_platform::collections::HashSet;
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
|
||||||
|
enum TestLabel {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
C,
|
||||||
|
D,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TestNode {
|
||||||
|
inputs: Vec<SlotInfo>,
|
||||||
|
outputs: Vec<SlotInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestNode {
|
||||||
|
pub fn new(inputs: usize, outputs: usize) -> Self {
|
||||||
|
TestNode {
|
||||||
|
inputs: (0..inputs)
|
||||||
|
.map(|i| SlotInfo::new(format!("in_{i}"), SlotType::TextureView))
|
||||||
|
.collect(),
|
||||||
|
outputs: (0..outputs)
|
||||||
|
.map(|i| SlotInfo::new(format!("out_{i}"), SlotType::TextureView))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for TestNode {
|
||||||
|
fn input(&self) -> Vec<SlotInfo> {
|
||||||
|
self.inputs.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output(&self) -> Vec<SlotInfo> {
|
||||||
|
self.outputs.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
_: &mut RenderGraphContext,
|
||||||
|
_: &mut RenderContext,
|
||||||
|
_: &World,
|
||||||
|
) -> Result<(), NodeRunError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_nodes(label: impl RenderLabel, graph: &RenderGraph) -> HashSet<InternedRenderLabel> {
|
||||||
|
graph
|
||||||
|
.iter_node_inputs(label)
|
||||||
|
.unwrap()
|
||||||
|
.map(|(_edge, node)| node.label)
|
||||||
|
.collect::<HashSet<InternedRenderLabel>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_nodes(label: impl RenderLabel, graph: &RenderGraph) -> HashSet<InternedRenderLabel> {
|
||||||
|
graph
|
||||||
|
.iter_node_outputs(label)
|
||||||
|
.unwrap()
|
||||||
|
.map(|(_edge, node)| node.label)
|
||||||
|
.collect::<HashSet<InternedRenderLabel>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_graph_edges() {
|
||||||
|
let mut graph = RenderGraph::default();
|
||||||
|
graph.add_node(TestLabel::A, TestNode::new(0, 1));
|
||||||
|
graph.add_node(TestLabel::B, TestNode::new(0, 1));
|
||||||
|
graph.add_node(TestLabel::C, TestNode::new(1, 1));
|
||||||
|
graph.add_node(TestLabel::D, TestNode::new(1, 0));
|
||||||
|
|
||||||
|
graph.add_slot_edge(TestLabel::A, "out_0", TestLabel::C, "in_0");
|
||||||
|
graph.add_node_edge(TestLabel::B, TestLabel::C);
|
||||||
|
graph.add_slot_edge(TestLabel::C, 0, TestLabel::D, 0);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
input_nodes(TestLabel::A, &graph).is_empty(),
|
||||||
|
"A has no inputs"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
output_nodes(TestLabel::A, &graph),
|
||||||
|
HashSet::from_iter((TestLabel::C,).into_array()),
|
||||||
|
"A outputs to C"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
input_nodes(TestLabel::B, &graph).is_empty(),
|
||||||
|
"B has no inputs"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
output_nodes(TestLabel::B, &graph),
|
||||||
|
HashSet::from_iter((TestLabel::C,).into_array()),
|
||||||
|
"B outputs to C"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
input_nodes(TestLabel::C, &graph),
|
||||||
|
HashSet::from_iter((TestLabel::A, TestLabel::B).into_array()),
|
||||||
|
"A and B input to C"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
output_nodes(TestLabel::C, &graph),
|
||||||
|
HashSet::from_iter((TestLabel::D,).into_array()),
|
||||||
|
"C outputs to D"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
input_nodes(TestLabel::D, &graph),
|
||||||
|
HashSet::from_iter((TestLabel::C,).into_array()),
|
||||||
|
"C inputs to D"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
output_nodes(TestLabel::D, &graph).is_empty(),
|
||||||
|
"D has no outputs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_node_typed() {
|
||||||
|
struct MyNode {
|
||||||
|
value: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for MyNode {
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
_: &mut RenderGraphContext,
|
||||||
|
_: &mut RenderContext,
|
||||||
|
_: &World,
|
||||||
|
) -> Result<(), NodeRunError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut graph = RenderGraph::default();
|
||||||
|
|
||||||
|
graph.add_node(TestLabel::A, MyNode { value: 42 });
|
||||||
|
|
||||||
|
let node: &MyNode = graph.get_node(TestLabel::A).unwrap();
|
||||||
|
assert_eq!(node.value, 42, "node value matches");
|
||||||
|
|
||||||
|
let result: Result<&TestNode, RenderGraphError> = graph.get_node(TestLabel::A);
|
||||||
|
assert_eq!(
|
||||||
|
result.unwrap_err(),
|
||||||
|
RenderGraphError::WrongNodeType,
|
||||||
|
"expect a wrong node type error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_slot_already_occupied() {
|
||||||
|
let mut graph = RenderGraph::default();
|
||||||
|
|
||||||
|
graph.add_node(TestLabel::A, TestNode::new(0, 1));
|
||||||
|
graph.add_node(TestLabel::B, TestNode::new(0, 1));
|
||||||
|
graph.add_node(TestLabel::C, TestNode::new(1, 1));
|
||||||
|
|
||||||
|
graph.add_slot_edge(TestLabel::A, 0, TestLabel::C, 0);
|
||||||
|
assert_eq!(
|
||||||
|
graph.try_add_slot_edge(TestLabel::B, 0, TestLabel::C, 0),
|
||||||
|
Err(RenderGraphError::NodeInputSlotAlreadyOccupied {
|
||||||
|
node: TestLabel::C.intern(),
|
||||||
|
input_slot: 0,
|
||||||
|
occupied_by_node: TestLabel::A.intern(),
|
||||||
|
}),
|
||||||
|
"Adding to a slot that is already occupied should return an error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edge_already_exists() {
|
||||||
|
let mut graph = RenderGraph::default();
|
||||||
|
|
||||||
|
graph.add_node(TestLabel::A, TestNode::new(0, 1));
|
||||||
|
graph.add_node(TestLabel::B, TestNode::new(1, 0));
|
||||||
|
|
||||||
|
graph.add_slot_edge(TestLabel::A, 0, TestLabel::B, 0);
|
||||||
|
assert_eq!(
|
||||||
|
graph.try_add_slot_edge(TestLabel::A, 0, TestLabel::B, 0),
|
||||||
|
Err(RenderGraphError::EdgeAlreadyExists(Edge::SlotEdge {
|
||||||
|
output_node: TestLabel::A.intern(),
|
||||||
|
output_index: 0,
|
||||||
|
input_node: TestLabel::B.intern(),
|
||||||
|
input_index: 0,
|
||||||
|
})),
|
||||||
|
"Adding to a duplicate edge should return an error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_node_edges() {
|
||||||
|
struct SimpleNode;
|
||||||
|
impl Node for SimpleNode {
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
_graph: &mut RenderGraphContext,
|
||||||
|
_render_context: &mut RenderContext,
|
||||||
|
_world: &World,
|
||||||
|
) -> Result<(), NodeRunError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromWorld for SimpleNode {
|
||||||
|
fn from_world(_world: &mut World) -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut graph = RenderGraph::default();
|
||||||
|
graph.add_node(TestLabel::A, SimpleNode);
|
||||||
|
graph.add_node(TestLabel::B, SimpleNode);
|
||||||
|
graph.add_node(TestLabel::C, SimpleNode);
|
||||||
|
|
||||||
|
graph.add_node_edges((TestLabel::A, TestLabel::B, TestLabel::C));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output_nodes(TestLabel::A, &graph),
|
||||||
|
HashSet::from_iter((TestLabel::B,).into_array()),
|
||||||
|
"A -> B"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
input_nodes(TestLabel::B, &graph),
|
||||||
|
HashSet::from_iter((TestLabel::A,).into_array()),
|
||||||
|
"A -> B"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
output_nodes(TestLabel::B, &graph),
|
||||||
|
HashSet::from_iter((TestLabel::C,).into_array()),
|
||||||
|
"B -> C"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
input_nodes(TestLabel::C, &graph),
|
||||||
|
HashSet::from_iter((TestLabel::B,).into_array()),
|
||||||
|
"B -> C"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
third_party/bevy_render/src/render_graph/mod.rs
vendored
Normal file
56
third_party/bevy_render/src/render_graph/mod.rs
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
mod app;
|
||||||
|
mod camera_driver_node;
|
||||||
|
mod context;
|
||||||
|
mod edge;
|
||||||
|
mod graph;
|
||||||
|
mod node;
|
||||||
|
mod node_slot;
|
||||||
|
|
||||||
|
pub use app::*;
|
||||||
|
pub use camera_driver_node::*;
|
||||||
|
pub use context::*;
|
||||||
|
pub use edge::*;
|
||||||
|
pub use graph::*;
|
||||||
|
pub use node::*;
|
||||||
|
pub use node_slot::*;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RenderGraphError {
|
||||||
|
#[error("node {0:?} does not exist")]
|
||||||
|
InvalidNode(InternedRenderLabel),
|
||||||
|
#[error("output node slot does not exist")]
|
||||||
|
InvalidOutputNodeSlot(SlotLabel),
|
||||||
|
#[error("input node slot does not exist")]
|
||||||
|
InvalidInputNodeSlot(SlotLabel),
|
||||||
|
#[error("node does not match the given type")]
|
||||||
|
WrongNodeType,
|
||||||
|
#[error("attempted to connect output slot {output_slot} from node {output_node:?} to incompatible input slot {input_slot} from node {input_node:?}")]
|
||||||
|
MismatchedNodeSlots {
|
||||||
|
output_node: InternedRenderLabel,
|
||||||
|
output_slot: usize,
|
||||||
|
input_node: InternedRenderLabel,
|
||||||
|
input_slot: usize,
|
||||||
|
},
|
||||||
|
#[error("attempted to add an edge that already exists")]
|
||||||
|
EdgeAlreadyExists(Edge),
|
||||||
|
#[error("attempted to remove an edge that does not exist")]
|
||||||
|
EdgeDoesNotExist(Edge),
|
||||||
|
#[error("node {node:?} has an unconnected input slot {input_slot}")]
|
||||||
|
UnconnectedNodeInputSlot {
|
||||||
|
node: InternedRenderLabel,
|
||||||
|
input_slot: usize,
|
||||||
|
},
|
||||||
|
#[error("node {node:?} has an unconnected output slot {output_slot}")]
|
||||||
|
UnconnectedNodeOutputSlot {
|
||||||
|
node: InternedRenderLabel,
|
||||||
|
output_slot: usize,
|
||||||
|
},
|
||||||
|
#[error("node {node:?} input slot {input_slot} already occupied by {occupied_by_node:?}")]
|
||||||
|
NodeInputSlotAlreadyOccupied {
|
||||||
|
node: InternedRenderLabel,
|
||||||
|
input_slot: usize,
|
||||||
|
occupied_by_node: InternedRenderLabel,
|
||||||
|
},
|
||||||
|
}
|
||||||
426
third_party/bevy_render/src/render_graph/node.rs
vendored
Normal file
426
third_party/bevy_render/src/render_graph/node.rs
vendored
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
use crate::{
|
||||||
|
render_graph::{
|
||||||
|
Edge, InputSlotError, OutputSlotError, RenderGraphContext, RenderGraphError,
|
||||||
|
RunSubGraphError, SlotInfo, SlotInfos,
|
||||||
|
},
|
||||||
|
render_phase::DrawError,
|
||||||
|
renderer::RenderContext,
|
||||||
|
};
|
||||||
|
pub use bevy_ecs::label::DynEq;
|
||||||
|
use bevy_ecs::{
|
||||||
|
define_label,
|
||||||
|
intern::Interned,
|
||||||
|
query::{QueryItem, QueryState, ReadOnlyQueryData},
|
||||||
|
world::{FromWorld, World},
|
||||||
|
};
|
||||||
|
use core::fmt::Debug;
|
||||||
|
use downcast_rs::{impl_downcast, Downcast};
|
||||||
|
use thiserror::Error;
|
||||||
|
use variadics_please::all_tuples_with_size;
|
||||||
|
|
||||||
|
pub use bevy_render_macros::RenderLabel;
|
||||||
|
|
||||||
|
use super::{InternedRenderSubGraph, RenderSubGraph};
|
||||||
|
|
||||||
|
define_label!(
|
||||||
|
#[diagnostic::on_unimplemented(
|
||||||
|
note = "consider annotating `{Self}` with `#[derive(RenderLabel)]`"
|
||||||
|
)]
|
||||||
|
/// A strongly-typed class of labels used to identify a [`Node`] in a render graph.
|
||||||
|
RenderLabel,
|
||||||
|
RENDER_LABEL_INTERNER
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A shorthand for `Interned<dyn RenderLabel>`.
|
||||||
|
pub type InternedRenderLabel = Interned<dyn RenderLabel>;
|
||||||
|
|
||||||
|
pub trait IntoRenderNodeArray<const N: usize> {
|
||||||
|
fn into_array(self) -> [InternedRenderLabel; N];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> IntoRenderNodeArray<N> for Vec<InternedRenderLabel> {
|
||||||
|
fn into_array(self) -> [InternedRenderLabel; N] {
|
||||||
|
self.try_into().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_render_label_tuples {
|
||||||
|
($N: expr, $(#[$meta:meta])* $(($T: ident, $I: ident)),*) => {
|
||||||
|
$(#[$meta])*
|
||||||
|
impl<$($T: RenderLabel),*> IntoRenderNodeArray<$N> for ($($T,)*) {
|
||||||
|
#[inline]
|
||||||
|
fn into_array(self) -> [InternedRenderLabel; $N] {
|
||||||
|
let ($($I,)*) = self;
|
||||||
|
[$($I.intern(), )*]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
all_tuples_with_size!(
|
||||||
|
#[doc(fake_variadic)]
|
||||||
|
impl_render_label_tuples,
|
||||||
|
1,
|
||||||
|
32,
|
||||||
|
T,
|
||||||
|
l
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A render node that can be added to a [`RenderGraph`](super::RenderGraph).
|
||||||
|
///
|
||||||
|
/// Nodes are the fundamental part of the graph and used to extend its functionality, by
|
||||||
|
/// generating draw calls and/or running subgraphs.
|
||||||
|
/// They are added via the `render_graph::add_node(my_node)` method.
|
||||||
|
///
|
||||||
|
/// To determine their position in the graph and ensure that all required dependencies (inputs)
|
||||||
|
/// are already executed, [`Edges`](Edge) are used.
|
||||||
|
///
|
||||||
|
/// A node can produce outputs used as dependencies by other nodes.
|
||||||
|
/// Those inputs and outputs are called slots and are the default way of passing render data
|
||||||
|
/// inside the graph. For more information see [`SlotType`](super::SlotType).
|
||||||
|
pub trait Node: Downcast + Send + Sync + 'static {
|
||||||
|
/// Specifies the required input slots for this node.
|
||||||
|
/// They will then be available during the run method inside the [`RenderGraphContext`].
|
||||||
|
fn input(&self) -> Vec<SlotInfo> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specifies the produced output slots for this node.
|
||||||
|
/// They can then be passed one inside [`RenderGraphContext`] during the run method.
|
||||||
|
fn output(&self) -> Vec<SlotInfo> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates internal node state using the current render [`World`] prior to the run method.
|
||||||
|
fn update(&mut self, _world: &mut World) {}
|
||||||
|
|
||||||
|
/// Runs the graph node logic, issues draw calls, updates the output slots and
|
||||||
|
/// optionally queues up subgraphs for execution. The graph data, input and output values are
|
||||||
|
/// passed via the [`RenderGraphContext`].
|
||||||
|
fn run<'w>(
|
||||||
|
&self,
|
||||||
|
graph: &mut RenderGraphContext,
|
||||||
|
render_context: &mut RenderContext<'w>,
|
||||||
|
world: &'w World,
|
||||||
|
) -> Result<(), NodeRunError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_downcast!(Node);
|
||||||
|
|
||||||
|
#[derive(Error, Debug, Eq, PartialEq)]
|
||||||
|
pub enum NodeRunError {
|
||||||
|
#[error("encountered an input slot error")]
|
||||||
|
InputSlotError(#[from] InputSlotError),
|
||||||
|
#[error("encountered an output slot error")]
|
||||||
|
OutputSlotError(#[from] OutputSlotError),
|
||||||
|
#[error("encountered an error when running a sub-graph")]
|
||||||
|
RunSubGraphError(#[from] RunSubGraphError),
|
||||||
|
#[error("encountered an error when executing draw command")]
|
||||||
|
DrawError(#[from] DrawError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of input and output [`Edges`](Edge) for a [`Node`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Edges {
|
||||||
|
label: InternedRenderLabel,
|
||||||
|
input_edges: Vec<Edge>,
|
||||||
|
output_edges: Vec<Edge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Edges {
|
||||||
|
/// Returns all "input edges" (edges going "in") for this node .
|
||||||
|
#[inline]
|
||||||
|
pub fn input_edges(&self) -> &[Edge] {
|
||||||
|
&self.input_edges
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all "output edges" (edges going "out") for this node .
|
||||||
|
#[inline]
|
||||||
|
pub fn output_edges(&self) -> &[Edge] {
|
||||||
|
&self.output_edges
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns this node's label.
|
||||||
|
#[inline]
|
||||||
|
pub fn label(&self) -> InternedRenderLabel {
|
||||||
|
self.label
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an edge to the `input_edges` if it does not already exist.
|
||||||
|
pub(crate) fn add_input_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> {
|
||||||
|
if self.has_input_edge(&edge) {
|
||||||
|
return Err(RenderGraphError::EdgeAlreadyExists(edge));
|
||||||
|
}
|
||||||
|
self.input_edges.push(edge);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an edge from the `input_edges` if it exists.
|
||||||
|
pub(crate) fn remove_input_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> {
|
||||||
|
if let Some(index) = self.input_edges.iter().position(|e| *e == edge) {
|
||||||
|
self.input_edges.swap_remove(index);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(RenderGraphError::EdgeDoesNotExist(edge))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an edge to the `output_edges` if it does not already exist.
|
||||||
|
pub(crate) fn add_output_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> {
|
||||||
|
if self.has_output_edge(&edge) {
|
||||||
|
return Err(RenderGraphError::EdgeAlreadyExists(edge));
|
||||||
|
}
|
||||||
|
self.output_edges.push(edge);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an edge from the `output_edges` if it exists.
|
||||||
|
pub(crate) fn remove_output_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> {
|
||||||
|
if let Some(index) = self.output_edges.iter().position(|e| *e == edge) {
|
||||||
|
self.output_edges.swap_remove(index);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(RenderGraphError::EdgeDoesNotExist(edge))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the input edge already exists.
|
||||||
|
pub fn has_input_edge(&self, edge: &Edge) -> bool {
|
||||||
|
self.input_edges.contains(edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the output edge already exists.
|
||||||
|
pub fn has_output_edge(&self, edge: &Edge) -> bool {
|
||||||
|
self.output_edges.contains(edge)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches the `input_edges` for a [`Edge::SlotEdge`],
|
||||||
|
/// which `input_index` matches the `index`;
|
||||||
|
pub fn get_input_slot_edge(&self, index: usize) -> Result<&Edge, RenderGraphError> {
|
||||||
|
self.input_edges
|
||||||
|
.iter()
|
||||||
|
.find(|e| {
|
||||||
|
if let Edge::SlotEdge { input_index, .. } = e {
|
||||||
|
*input_index == index
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or(RenderGraphError::UnconnectedNodeInputSlot {
|
||||||
|
input_slot: index,
|
||||||
|
node: self.label,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches the `output_edges` for a [`Edge::SlotEdge`],
|
||||||
|
/// which `output_index` matches the `index`;
|
||||||
|
pub fn get_output_slot_edge(&self, index: usize) -> Result<&Edge, RenderGraphError> {
|
||||||
|
self.output_edges
|
||||||
|
.iter()
|
||||||
|
.find(|e| {
|
||||||
|
if let Edge::SlotEdge { output_index, .. } = e {
|
||||||
|
*output_index == index
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or(RenderGraphError::UnconnectedNodeOutputSlot {
|
||||||
|
output_slot: index,
|
||||||
|
node: self.label,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The internal representation of a [`Node`], with all data required
|
||||||
|
/// by the [`RenderGraph`](super::RenderGraph).
|
||||||
|
///
|
||||||
|
/// The `input_slots` and `output_slots` are provided by the `node`.
|
||||||
|
pub struct NodeState {
|
||||||
|
pub label: InternedRenderLabel,
|
||||||
|
/// The name of the type that implements [`Node`].
|
||||||
|
pub type_name: &'static str,
|
||||||
|
pub node: Box<dyn Node>,
|
||||||
|
pub input_slots: SlotInfos,
|
||||||
|
pub output_slots: SlotInfos,
|
||||||
|
pub edges: Edges,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for NodeState {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
writeln!(f, "{:?} ({})", self.label, self.type_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeState {
|
||||||
|
/// Creates an [`NodeState`] without edges, but the `input_slots` and `output_slots`
|
||||||
|
/// are provided by the `node`.
|
||||||
|
pub fn new<T>(label: InternedRenderLabel, node: T) -> Self
|
||||||
|
where
|
||||||
|
T: Node,
|
||||||
|
{
|
||||||
|
NodeState {
|
||||||
|
label,
|
||||||
|
input_slots: node.input().into(),
|
||||||
|
output_slots: node.output().into(),
|
||||||
|
node: Box::new(node),
|
||||||
|
type_name: core::any::type_name::<T>(),
|
||||||
|
edges: Edges {
|
||||||
|
label,
|
||||||
|
input_edges: Vec::new(),
|
||||||
|
output_edges: Vec::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [`Node`].
|
||||||
|
pub fn node<T>(&self) -> Result<&T, RenderGraphError>
|
||||||
|
where
|
||||||
|
T: Node,
|
||||||
|
{
|
||||||
|
self.node
|
||||||
|
.downcast_ref::<T>()
|
||||||
|
.ok_or(RenderGraphError::WrongNodeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [`Node`] mutably.
|
||||||
|
pub fn node_mut<T>(&mut self) -> Result<&mut T, RenderGraphError>
|
||||||
|
where
|
||||||
|
T: Node,
|
||||||
|
{
|
||||||
|
self.node
|
||||||
|
.downcast_mut::<T>()
|
||||||
|
.ok_or(RenderGraphError::WrongNodeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that each input slot corresponds to an input edge.
|
||||||
|
pub fn validate_input_slots(&self) -> Result<(), RenderGraphError> {
|
||||||
|
for i in 0..self.input_slots.len() {
|
||||||
|
self.edges.get_input_slot_edge(i)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that each output slot corresponds to an output edge.
|
||||||
|
pub fn validate_output_slots(&self) -> Result<(), RenderGraphError> {
|
||||||
|
for i in 0..self.output_slots.len() {
|
||||||
|
self.edges.get_output_slot_edge(i)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Node`] without any inputs, outputs and subgraphs, which does nothing when run.
|
||||||
|
/// Used (as a label) to bundle multiple dependencies into one inside
|
||||||
|
/// the [`RenderGraph`](super::RenderGraph).
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct EmptyNode;
|
||||||
|
|
||||||
|
impl Node for EmptyNode {
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
_graph: &mut RenderGraphContext,
|
||||||
|
_render_context: &mut RenderContext,
|
||||||
|
_world: &World,
|
||||||
|
) -> Result<(), NodeRunError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`RenderGraph`](super::RenderGraph) [`Node`] that runs the configured subgraph once.
|
||||||
|
/// This makes it easier to insert sub-graph runs into a graph.
|
||||||
|
pub struct RunGraphOnViewNode {
|
||||||
|
sub_graph: InternedRenderSubGraph,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunGraphOnViewNode {
|
||||||
|
pub fn new<T: RenderSubGraph>(sub_graph: T) -> Self {
|
||||||
|
Self {
|
||||||
|
sub_graph: sub_graph.intern(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for RunGraphOnViewNode {
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
graph: &mut RenderGraphContext,
|
||||||
|
_render_context: &mut RenderContext,
|
||||||
|
_world: &World,
|
||||||
|
) -> Result<(), NodeRunError> {
|
||||||
|
graph.run_sub_graph(self.sub_graph, vec![], Some(graph.view_entity()), None)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This trait should be used instead of the [`Node`] trait when making a render node that runs on a view.
|
||||||
|
///
|
||||||
|
/// It is intended to be used with [`ViewNodeRunner`]
|
||||||
|
pub trait ViewNode {
|
||||||
|
/// The query that will be used on the view entity.
|
||||||
|
/// It is guaranteed to run on the view entity, so there's no need for a filter
|
||||||
|
type ViewQuery: ReadOnlyQueryData;
|
||||||
|
|
||||||
|
/// Updates internal node state using the current render [`World`] prior to the run method.
|
||||||
|
fn update(&mut self, _world: &mut World) {}
|
||||||
|
|
||||||
|
/// Runs the graph node logic, issues draw calls, updates the output slots and
|
||||||
|
/// optionally queues up subgraphs for execution. The graph data, input and output values are
|
||||||
|
/// passed via the [`RenderGraphContext`].
|
||||||
|
fn run<'w>(
|
||||||
|
&self,
|
||||||
|
graph: &mut RenderGraphContext,
|
||||||
|
render_context: &mut RenderContext<'w>,
|
||||||
|
view_query: QueryItem<'w, '_, Self::ViewQuery>,
|
||||||
|
world: &'w World,
|
||||||
|
) -> Result<(), NodeRunError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This [`Node`] can be used to run any [`ViewNode`].
|
||||||
|
/// It will take care of updating the view query in `update()` and running the query in `run()`.
|
||||||
|
///
|
||||||
|
/// This [`Node`] exists to help reduce boilerplate when making a render node that runs on a view.
|
||||||
|
pub struct ViewNodeRunner<N: ViewNode> {
|
||||||
|
view_query: QueryState<N::ViewQuery>,
|
||||||
|
node: N,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: ViewNode> ViewNodeRunner<N> {
|
||||||
|
pub fn new(node: N, world: &mut World) -> Self {
|
||||||
|
Self {
|
||||||
|
view_query: world.query_filtered(),
|
||||||
|
node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N: ViewNode + FromWorld> FromWorld for ViewNodeRunner<N> {
|
||||||
|
fn from_world(world: &mut World) -> Self {
|
||||||
|
Self::new(N::from_world(world), world)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Node for ViewNodeRunner<T>
|
||||||
|
where
|
||||||
|
T: ViewNode + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn update(&mut self, world: &mut World) {
|
||||||
|
self.view_query.update_archetypes(world);
|
||||||
|
self.node.update(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run<'w>(
|
||||||
|
&self,
|
||||||
|
graph: &mut RenderGraphContext,
|
||||||
|
render_context: &mut RenderContext<'w>,
|
||||||
|
world: &'w World,
|
||||||
|
) -> Result<(), NodeRunError> {
|
||||||
|
let Ok(view) = self.view_query.get_manual(world, graph.view_entity()) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewNode::run(&self.node, graph, render_context, view, world)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
165
third_party/bevy_render/src/render_graph/node_slot.rs
vendored
Normal file
165
third_party/bevy_render/src/render_graph/node_slot.rs
vendored
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
use alloc::borrow::Cow;
|
||||||
|
use bevy_ecs::entity::Entity;
|
||||||
|
use core::fmt;
|
||||||
|
use derive_more::derive::From;
|
||||||
|
|
||||||
|
use crate::render_resource::{Buffer, Sampler, TextureView};
|
||||||
|
|
||||||
|
/// A value passed between render [`Nodes`](super::Node).
|
||||||
|
/// Corresponds to the [`SlotType`] specified in the [`RenderGraph`](super::RenderGraph).
|
||||||
|
///
|
||||||
|
/// Slots can have four different types of values:
|
||||||
|
/// [`Buffer`], [`TextureView`], [`Sampler`] and [`Entity`].
|
||||||
|
///
|
||||||
|
/// These values do not contain the actual render data, but only the ids to retrieve them.
|
||||||
|
#[derive(Debug, Clone, From)]
|
||||||
|
pub enum SlotValue {
|
||||||
|
/// A GPU-accessible [`Buffer`].
|
||||||
|
Buffer(Buffer),
|
||||||
|
/// A [`TextureView`] describes a texture used in a pipeline.
|
||||||
|
TextureView(TextureView),
|
||||||
|
/// A texture [`Sampler`] defines how a pipeline will sample from a [`TextureView`].
|
||||||
|
Sampler(Sampler),
|
||||||
|
/// An entity from the ECS.
|
||||||
|
Entity(Entity),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlotValue {
|
||||||
|
/// Returns the [`SlotType`] of this value.
|
||||||
|
pub fn slot_type(&self) -> SlotType {
|
||||||
|
match self {
|
||||||
|
SlotValue::Buffer(_) => SlotType::Buffer,
|
||||||
|
SlotValue::TextureView(_) => SlotType::TextureView,
|
||||||
|
SlotValue::Sampler(_) => SlotType::Sampler,
|
||||||
|
SlotValue::Entity(_) => SlotType::Entity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the render resources created (output) or used (input) by
|
||||||
|
/// the render [`Nodes`](super::Node).
|
||||||
|
///
|
||||||
|
/// This should not be confused with [`SlotValue`], which actually contains the passed data.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum SlotType {
|
||||||
|
/// A GPU-accessible [`Buffer`].
|
||||||
|
Buffer,
|
||||||
|
/// A [`TextureView`] describes a texture used in a pipeline.
|
||||||
|
TextureView,
|
||||||
|
/// A texture [`Sampler`] defines how a pipeline will sample from a [`TextureView`].
|
||||||
|
Sampler,
|
||||||
|
/// An entity from the ECS.
|
||||||
|
Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SlotType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
SlotType::Buffer => "Buffer",
|
||||||
|
SlotType::TextureView => "TextureView",
|
||||||
|
SlotType::Sampler => "Sampler",
|
||||||
|
SlotType::Entity => "Entity",
|
||||||
|
};
|
||||||
|
|
||||||
|
f.write_str(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`SlotLabel`] is used to reference a slot by either its name or index
|
||||||
|
/// inside the [`RenderGraph`](super::RenderGraph).
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, From)]
|
||||||
|
pub enum SlotLabel {
|
||||||
|
Index(usize),
|
||||||
|
Name(Cow<'static, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SlotLabel> for SlotLabel {
|
||||||
|
fn from(value: &SlotLabel) -> Self {
|
||||||
|
value.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for SlotLabel {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
SlotLabel::Name(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for SlotLabel {
|
||||||
|
fn from(value: &'static str) -> Self {
|
||||||
|
SlotLabel::Name(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The internal representation of a slot, which specifies its [`SlotType`] and name.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SlotInfo {
|
||||||
|
pub name: Cow<'static, str>,
|
||||||
|
pub slot_type: SlotType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlotInfo {
|
||||||
|
pub fn new(name: impl Into<Cow<'static, str>>, slot_type: SlotType) -> Self {
|
||||||
|
SlotInfo {
|
||||||
|
name: name.into(),
|
||||||
|
slot_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of input or output [`SlotInfos`](SlotInfo) for
|
||||||
|
/// a [`NodeState`](super::NodeState).
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct SlotInfos {
|
||||||
|
slots: Vec<SlotInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: IntoIterator<Item = SlotInfo>> From<T> for SlotInfos {
|
||||||
|
fn from(slots: T) -> Self {
|
||||||
|
SlotInfos {
|
||||||
|
slots: slots.into_iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlotInfos {
|
||||||
|
/// Returns the count of slots.
|
||||||
|
#[inline]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.slots.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there are no slots.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.slots.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [`SlotInfo`] for the provided label.
|
||||||
|
pub fn get_slot(&self, label: impl Into<SlotLabel>) -> Option<&SlotInfo> {
|
||||||
|
let label = label.into();
|
||||||
|
let index = self.get_slot_index(label)?;
|
||||||
|
self.slots.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [`SlotInfo`] for the provided label mutably.
|
||||||
|
pub fn get_slot_mut(&mut self, label: impl Into<SlotLabel>) -> Option<&mut SlotInfo> {
|
||||||
|
let label = label.into();
|
||||||
|
let index = self.get_slot_index(label)?;
|
||||||
|
self.slots.get_mut(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the index (inside input or output slots) of the slot for the provided label.
|
||||||
|
pub fn get_slot_index(&self, label: impl Into<SlotLabel>) -> Option<usize> {
|
||||||
|
let label = label.into();
|
||||||
|
match label {
|
||||||
|
SlotLabel::Index(index) => Some(index),
|
||||||
|
SlotLabel::Name(ref name) => self.slots.iter().position(|s| s.name == *name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the slot infos.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &SlotInfo> {
|
||||||
|
self.slots.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
396
third_party/bevy_render/src/render_phase/draw.rs
vendored
Normal file
396
third_party/bevy_render/src/render_phase/draw.rs
vendored
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
use crate::render_phase::{PhaseItem, TrackedRenderPass};
|
||||||
|
use bevy_app::{App, SubApp};
|
||||||
|
use bevy_ecs::{
|
||||||
|
entity::Entity,
|
||||||
|
query::{QueryEntityError, QueryState, ROQueryItem, ReadOnlyQueryData},
|
||||||
|
resource::Resource,
|
||||||
|
system::{ReadOnlySystemParam, SystemParam, SystemParamItem, SystemState},
|
||||||
|
world::World,
|
||||||
|
};
|
||||||
|
use bevy_utils::TypeIdMap;
|
||||||
|
use core::{any::TypeId, fmt::Debug, hash::Hash};
|
||||||
|
use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
use thiserror::Error;
|
||||||
|
use variadics_please::all_tuples;
|
||||||
|
|
||||||
|
/// A draw function used to draw [`PhaseItem`]s.
|
||||||
|
///
|
||||||
|
/// The draw function can retrieve and query the required ECS data from the render world.
|
||||||
|
///
|
||||||
|
/// This trait can either be implemented directly or implicitly composed out of multiple modular
|
||||||
|
/// [`RenderCommand`]s. For more details and an example see the [`RenderCommand`] documentation.
|
||||||
|
pub trait Draw<P: PhaseItem>: Send + Sync + 'static {
|
||||||
|
/// Prepares the draw function to be used. This is called once and only once before the phase
|
||||||
|
/// begins. There may be zero or more [`draw`](Draw::draw) calls following a call to this function.
|
||||||
|
/// Implementing this is optional.
|
||||||
|
#[expect(
|
||||||
|
unused_variables,
|
||||||
|
reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion."
|
||||||
|
)]
|
||||||
|
fn prepare(&mut self, world: &'_ World) {}
|
||||||
|
|
||||||
|
/// Draws a [`PhaseItem`] by issuing zero or more `draw` calls via the [`TrackedRenderPass`].
|
||||||
|
fn draw<'w>(
|
||||||
|
&mut self,
|
||||||
|
world: &'w World,
|
||||||
|
pass: &mut TrackedRenderPass<'w>,
|
||||||
|
view: Entity,
|
||||||
|
item: &P,
|
||||||
|
) -> Result<(), DrawError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug, PartialEq, Eq)]
|
||||||
|
pub enum DrawError {
|
||||||
|
#[error("Failed to execute render command {0:?}")]
|
||||||
|
RenderCommandFailure(&'static str),
|
||||||
|
#[error("Failed to get execute view query")]
|
||||||
|
InvalidViewQuery,
|
||||||
|
#[error("View entity not found")]
|
||||||
|
ViewEntityNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make this generic?
|
||||||
|
/// An identifier for a [`Draw`] function stored in [`DrawFunctions`].
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct DrawFunctionId(u32);
|
||||||
|
|
||||||
|
/// Stores all [`Draw`] functions for the [`PhaseItem`] type.
|
||||||
|
///
|
||||||
|
/// For retrieval, the [`Draw`] functions are mapped to their respective [`TypeId`]s.
|
||||||
|
pub struct DrawFunctionsInternal<P: PhaseItem> {
|
||||||
|
pub draw_functions: Vec<Box<dyn Draw<P>>>,
|
||||||
|
pub indices: TypeIdMap<DrawFunctionId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PhaseItem> DrawFunctionsInternal<P> {
|
||||||
|
/// Prepares all draw function. This is called once and only once before the phase begins.
|
||||||
|
pub fn prepare(&mut self, world: &World) {
|
||||||
|
for function in &mut self.draw_functions {
|
||||||
|
function.prepare(world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the [`Draw`] function and maps it to its own type.
|
||||||
|
pub fn add<T: Draw<P>>(&mut self, draw_function: T) -> DrawFunctionId {
|
||||||
|
self.add_with::<T, T>(draw_function)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the [`Draw`] function and maps it to the type `T`
|
||||||
|
pub fn add_with<T: 'static, D: Draw<P>>(&mut self, draw_function: D) -> DrawFunctionId {
|
||||||
|
let id = DrawFunctionId(self.draw_functions.len().try_into().unwrap());
|
||||||
|
self.draw_functions.push(Box::new(draw_function));
|
||||||
|
self.indices.insert(TypeId::of::<T>(), id);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the [`Draw`] function corresponding to the `id` mutably.
|
||||||
|
pub fn get_mut(&mut self, id: DrawFunctionId) -> Option<&mut dyn Draw<P>> {
|
||||||
|
self.draw_functions.get_mut(id.0 as usize).map(|f| &mut **f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the id of the [`Draw`] function corresponding to their associated type `T`.
|
||||||
|
pub fn get_id<T: 'static>(&self) -> Option<DrawFunctionId> {
|
||||||
|
self.indices.get(&TypeId::of::<T>()).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the id of the [`Draw`] function corresponding to their associated type `T`.
|
||||||
|
///
|
||||||
|
/// Fallible wrapper for [`Self::get_id()`]
|
||||||
|
///
|
||||||
|
/// ## Panics
|
||||||
|
/// If the id doesn't exist, this function will panic.
|
||||||
|
pub fn id<T: 'static>(&self) -> DrawFunctionId {
|
||||||
|
self.get_id::<T>().unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"Draw function {} not found for {}",
|
||||||
|
core::any::type_name::<T>(),
|
||||||
|
core::any::type_name::<P>()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores all draw functions for the [`PhaseItem`] type hidden behind a reader-writer lock.
|
||||||
|
///
|
||||||
|
/// To access them the [`DrawFunctions::read`] and [`DrawFunctions::write`] methods are used.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct DrawFunctions<P: PhaseItem> {
|
||||||
|
internal: RwLock<DrawFunctionsInternal<P>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PhaseItem> Default for DrawFunctions<P> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
internal: RwLock::new(DrawFunctionsInternal {
|
||||||
|
draw_functions: Vec::new(),
|
||||||
|
indices: Default::default(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PhaseItem> DrawFunctions<P> {
|
||||||
|
/// Accesses the draw functions in read mode.
|
||||||
|
pub fn read(&self) -> RwLockReadGuard<'_, DrawFunctionsInternal<P>> {
|
||||||
|
self.internal.read().unwrap_or_else(PoisonError::into_inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accesses the draw functions in write mode.
|
||||||
|
pub fn write(&self) -> RwLockWriteGuard<'_, DrawFunctionsInternal<P>> {
|
||||||
|
self.internal
|
||||||
|
.write()
|
||||||
|
.unwrap_or_else(PoisonError::into_inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`RenderCommand`]s are modular standardized pieces of render logic that can be composed into
|
||||||
|
/// [`Draw`] functions.
|
||||||
|
///
|
||||||
|
/// To turn a stateless render command into a usable draw function it has to be wrapped by a
|
||||||
|
/// [`RenderCommandState`].
|
||||||
|
/// This is done automatically when registering a render command as a [`Draw`] function via the
|
||||||
|
/// [`AddRenderCommand::add_render_command`] method.
|
||||||
|
///
|
||||||
|
/// Compared to the draw function the required ECS data is fetched automatically
|
||||||
|
/// (by the [`RenderCommandState`]) from the render world.
|
||||||
|
/// Therefore the three types [`Param`](RenderCommand::Param),
|
||||||
|
/// [`ViewQuery`](RenderCommand::ViewQuery) and
|
||||||
|
/// [`ItemQuery`](RenderCommand::ItemQuery) are used.
|
||||||
|
/// They specify which information is required to execute the render command.
|
||||||
|
///
|
||||||
|
/// Multiple render commands can be combined together by wrapping them in a tuple.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// The `DrawMaterial` draw function is created from the following render command
|
||||||
|
/// tuple. Const generics are used to set specific bind group locations:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_render::render_phase::SetItemPipeline;
|
||||||
|
/// # struct SetMeshViewBindGroup<const N: usize>;
|
||||||
|
/// # struct SetMeshViewBindingArrayBindGroup<const N: usize>;
|
||||||
|
/// # struct SetMeshBindGroup<const N: usize>;
|
||||||
|
/// # struct SetMaterialBindGroup<M, const N: usize>(std::marker::PhantomData<M>);
|
||||||
|
/// # struct DrawMesh;
|
||||||
|
/// pub type DrawMaterial<M> = (
|
||||||
|
/// SetItemPipeline,
|
||||||
|
/// SetMeshViewBindGroup<0>,
|
||||||
|
/// SetMeshViewBindingArrayBindGroup<1>,
|
||||||
|
/// SetMeshBindGroup<2>,
|
||||||
|
/// SetMaterialBindGroup<M, 3>,
|
||||||
|
/// DrawMesh,
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub trait RenderCommand<P: PhaseItem> {
|
||||||
|
/// Specifies the general ECS data (e.g. resources) required by [`RenderCommand::render`].
|
||||||
|
///
|
||||||
|
/// When fetching resources, note that, due to lifetime limitations of the `Deref` trait,
|
||||||
|
/// [`SRes::into_inner`] must be called on each [`SRes`] reference in the
|
||||||
|
/// [`RenderCommand::render`] method, instead of being automatically dereferenced as is the
|
||||||
|
/// case in normal `systems`.
|
||||||
|
///
|
||||||
|
/// All parameters have to be read only.
|
||||||
|
///
|
||||||
|
/// [`SRes`]: bevy_ecs::system::lifetimeless::SRes
|
||||||
|
/// [`SRes::into_inner`]: bevy_ecs::system::lifetimeless::SRes::into_inner
|
||||||
|
type Param: SystemParam + 'static;
|
||||||
|
/// Specifies the ECS data of the view entity required by [`RenderCommand::render`].
|
||||||
|
///
|
||||||
|
/// The view entity refers to the camera, or shadow-casting light, etc. from which the phase
|
||||||
|
/// item will be rendered from.
|
||||||
|
/// All components have to be accessed read only.
|
||||||
|
type ViewQuery: ReadOnlyQueryData;
|
||||||
|
/// Specifies the ECS data of the item entity required by [`RenderCommand::render`].
|
||||||
|
///
|
||||||
|
/// The item is the entity that will be rendered for the corresponding view.
|
||||||
|
/// All components have to be accessed read only.
|
||||||
|
///
|
||||||
|
/// For efficiency reasons, Bevy doesn't always extract entities to the
|
||||||
|
/// render world; for instance, entities that simply consist of meshes are
|
||||||
|
/// often not extracted. If the entity doesn't exist in the render world,
|
||||||
|
/// the supplied query data will be `None`.
|
||||||
|
type ItemQuery: ReadOnlyQueryData;
|
||||||
|
|
||||||
|
/// Renders a [`PhaseItem`] by recording commands (e.g. setting pipelines, binding bind groups,
|
||||||
|
/// issuing draw calls, etc.) via the [`TrackedRenderPass`].
|
||||||
|
fn render<'w>(
|
||||||
|
item: &P,
|
||||||
|
view: ROQueryItem<'w, '_, Self::ViewQuery>,
|
||||||
|
entity: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
|
||||||
|
param: SystemParamItem<'w, '_, Self::Param>,
|
||||||
|
pass: &mut TrackedRenderPass<'w>,
|
||||||
|
) -> RenderCommandResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a [`RenderCommand`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RenderCommandResult {
|
||||||
|
Success,
|
||||||
|
Skip,
|
||||||
|
Failure(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! render_command_tuple_impl {
|
||||||
|
($(#[$meta:meta])* $(($name: ident, $view: ident, $entity: ident)),*) => {
|
||||||
|
$(#[$meta])*
|
||||||
|
impl<P: PhaseItem, $($name: RenderCommand<P>),*> RenderCommand<P> for ($($name,)*) {
|
||||||
|
type Param = ($($name::Param,)*);
|
||||||
|
type ViewQuery = ($($name::ViewQuery,)*);
|
||||||
|
type ItemQuery = ($($name::ItemQuery,)*);
|
||||||
|
|
||||||
|
#[expect(
|
||||||
|
clippy::allow_attributes,
|
||||||
|
reason = "We are in a macro; as such, `non_snake_case` may not always lint."
|
||||||
|
)]
|
||||||
|
#[allow(
|
||||||
|
non_snake_case,
|
||||||
|
reason = "Parameter and variable names are provided by the macro invocation, not by us."
|
||||||
|
)]
|
||||||
|
fn render<'w>(
|
||||||
|
_item: &P,
|
||||||
|
($($view,)*): ROQueryItem<'w, '_, Self::ViewQuery>,
|
||||||
|
maybe_entities: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
|
||||||
|
($($name,)*): SystemParamItem<'w, '_, Self::Param>,
|
||||||
|
_pass: &mut TrackedRenderPass<'w>,
|
||||||
|
) -> RenderCommandResult {
|
||||||
|
match maybe_entities {
|
||||||
|
None => {
|
||||||
|
$(
|
||||||
|
match $name::render(_item, $view, None, $name, _pass) {
|
||||||
|
RenderCommandResult::Skip => return RenderCommandResult::Skip,
|
||||||
|
RenderCommandResult::Failure(reason) => return RenderCommandResult::Failure(reason),
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
Some(($($entity,)*)) => {
|
||||||
|
$(
|
||||||
|
match $name::render(_item, $view, Some($entity), $name, _pass) {
|
||||||
|
RenderCommandResult::Skip => return RenderCommandResult::Skip,
|
||||||
|
RenderCommandResult::Failure(reason) => return RenderCommandResult::Failure(reason),
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RenderCommandResult::Success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
all_tuples!(
|
||||||
|
#[doc(fake_variadic)]
|
||||||
|
render_command_tuple_impl,
|
||||||
|
0,
|
||||||
|
15,
|
||||||
|
C,
|
||||||
|
V,
|
||||||
|
E
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Wraps a [`RenderCommand`] into a state so that it can be used as a [`Draw`] function.
|
||||||
|
///
|
||||||
|
/// The [`RenderCommand::Param`], [`RenderCommand::ViewQuery`] and
|
||||||
|
/// [`RenderCommand::ItemQuery`] are fetched from the ECS and passed to the command.
|
||||||
|
pub struct RenderCommandState<P: PhaseItem + 'static, C: RenderCommand<P>> {
|
||||||
|
state: SystemState<C::Param>,
|
||||||
|
view: QueryState<C::ViewQuery>,
|
||||||
|
entity: QueryState<C::ItemQuery>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PhaseItem, C: RenderCommand<P>> RenderCommandState<P, C> {
|
||||||
|
/// Creates a new [`RenderCommandState`] for the [`RenderCommand`].
|
||||||
|
pub fn new(world: &mut World) -> Self {
|
||||||
|
Self {
|
||||||
|
state: SystemState::new(world),
|
||||||
|
view: world.query(),
|
||||||
|
entity: world.query(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: PhaseItem, C: RenderCommand<P> + Send + Sync + 'static> Draw<P> for RenderCommandState<P, C>
|
||||||
|
where
|
||||||
|
C::Param: ReadOnlySystemParam,
|
||||||
|
{
|
||||||
|
/// Prepares the render command to be used. This is called once and only once before the phase
|
||||||
|
/// begins. There may be zero or more [`draw`](RenderCommandState::draw) calls following a call to this function.
|
||||||
|
fn prepare(&mut self, world: &'_ World) {
|
||||||
|
self.view.update_archetypes(world);
|
||||||
|
self.entity.update_archetypes(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the ECS parameters for the wrapped [`RenderCommand`] and then renders it.
|
||||||
|
fn draw<'w>(
|
||||||
|
&mut self,
|
||||||
|
world: &'w World,
|
||||||
|
pass: &mut TrackedRenderPass<'w>,
|
||||||
|
view: Entity,
|
||||||
|
item: &P,
|
||||||
|
) -> Result<(), DrawError> {
|
||||||
|
let param = self.state.get(world);
|
||||||
|
let view = match self.view.get_manual(world, view) {
|
||||||
|
Ok(view) => view,
|
||||||
|
Err(err) => match err {
|
||||||
|
QueryEntityError::NotSpawned(_) => return Err(DrawError::ViewEntityNotFound),
|
||||||
|
QueryEntityError::QueryDoesNotMatch(_, _)
|
||||||
|
| QueryEntityError::AliasedMutability(_) => {
|
||||||
|
return Err(DrawError::InvalidViewQuery)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let entity = self.entity.get_manual(world, item.entity()).ok();
|
||||||
|
match C::render(item, view, entity, param, pass) {
|
||||||
|
RenderCommandResult::Success | RenderCommandResult::Skip => Ok(()),
|
||||||
|
RenderCommandResult::Failure(reason) => Err(DrawError::RenderCommandFailure(reason)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a [`RenderCommand`] as a [`Draw`] function.
|
||||||
|
/// They are stored inside the [`DrawFunctions`] resource of the app.
|
||||||
|
pub trait AddRenderCommand {
|
||||||
|
/// Adds the [`RenderCommand`] for the specified render phase to the app.
|
||||||
|
fn add_render_command<P: PhaseItem, C: RenderCommand<P> + Send + Sync + 'static>(
|
||||||
|
&mut self,
|
||||||
|
) -> &mut Self
|
||||||
|
where
|
||||||
|
C::Param: ReadOnlySystemParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddRenderCommand for SubApp {
|
||||||
|
fn add_render_command<P: PhaseItem, C: RenderCommand<P> + Send + Sync + 'static>(
|
||||||
|
&mut self,
|
||||||
|
) -> &mut Self
|
||||||
|
where
|
||||||
|
C::Param: ReadOnlySystemParam,
|
||||||
|
{
|
||||||
|
let draw_function = RenderCommandState::<P, C>::new(self.world_mut());
|
||||||
|
let draw_functions = self
|
||||||
|
.world()
|
||||||
|
.get_resource::<DrawFunctions<P>>()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"DrawFunctions<{}> must be added to the world as a resource \
|
||||||
|
before adding render commands to it",
|
||||||
|
core::any::type_name::<P>(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
draw_functions.write().add_with::<C, _>(draw_function);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddRenderCommand for App {
|
||||||
|
fn add_render_command<P: PhaseItem, C: RenderCommand<P> + Send + Sync + 'static>(
|
||||||
|
&mut self,
|
||||||
|
) -> &mut Self
|
||||||
|
where
|
||||||
|
C::Param: ReadOnlySystemParam,
|
||||||
|
{
|
||||||
|
SubApp::add_render_command::<P, C>(self.main_mut());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
671
third_party/bevy_render/src/render_phase/draw_state.rs
vendored
Normal file
671
third_party/bevy_render/src/render_phase/draw_state.rs
vendored
Normal file
@ -0,0 +1,671 @@
|
|||||||
|
use crate::{
|
||||||
|
diagnostic::internal::{Pass, PassKind, WritePipelineStatistics, WriteTimestamp},
|
||||||
|
render_resource::{
|
||||||
|
BindGroup, BindGroupId, Buffer, BufferId, BufferSlice, RenderPipeline, RenderPipelineId,
|
||||||
|
ShaderStages,
|
||||||
|
},
|
||||||
|
renderer::RenderDevice,
|
||||||
|
};
|
||||||
|
use bevy_camera::Viewport;
|
||||||
|
use bevy_color::LinearRgba;
|
||||||
|
use bevy_utils::default;
|
||||||
|
use core::ops::Range;
|
||||||
|
use wgpu::{IndexFormat, QuerySet, RenderPass};
|
||||||
|
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
type BufferSliceKey = (BufferId, wgpu::BufferAddress, wgpu::BufferSize);
|
||||||
|
|
||||||
|
/// Tracks the state of a [`TrackedRenderPass`].
|
||||||
|
///
|
||||||
|
/// This is used to skip redundant operations on the [`TrackedRenderPass`] (e.g. setting an already
|
||||||
|
/// set pipeline, binding an already bound bind group). These operations can otherwise be fairly
|
||||||
|
/// costly due to IO to the GPU, so deduplicating these calls results in a speedup.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct DrawState {
|
||||||
|
pipeline: Option<RenderPipelineId>,
|
||||||
|
bind_groups: Vec<(Option<BindGroupId>, Vec<u32>)>,
|
||||||
|
/// List of vertex buffers by [`BufferId`], offset, and size. See [`DrawState::buffer_slice_key`]
|
||||||
|
vertex_buffers: Vec<Option<BufferSliceKey>>,
|
||||||
|
index_buffer: Option<(BufferSliceKey, IndexFormat)>,
|
||||||
|
|
||||||
|
/// Stores whether this state is populated or empty for quick state invalidation
|
||||||
|
stores_state: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawState {
|
||||||
|
/// Marks the `pipeline` as bound.
|
||||||
|
fn set_pipeline(&mut self, pipeline: RenderPipelineId) {
|
||||||
|
// TODO: do these need to be cleared?
|
||||||
|
// self.bind_groups.clear();
|
||||||
|
// self.vertex_buffers.clear();
|
||||||
|
// self.index_buffer = None;
|
||||||
|
self.pipeline = Some(pipeline);
|
||||||
|
self.stores_state = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks, whether the `pipeline` is already bound.
|
||||||
|
fn is_pipeline_set(&self, pipeline: RenderPipelineId) -> bool {
|
||||||
|
self.pipeline == Some(pipeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the `bind_group` as bound to the `index`.
|
||||||
|
fn set_bind_group(&mut self, index: usize, bind_group: BindGroupId, dynamic_indices: &[u32]) {
|
||||||
|
let group = &mut self.bind_groups[index];
|
||||||
|
group.0 = Some(bind_group);
|
||||||
|
group.1.clear();
|
||||||
|
group.1.extend(dynamic_indices);
|
||||||
|
self.stores_state = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks, whether the `bind_group` is already bound to the `index`.
|
||||||
|
fn is_bind_group_set(
|
||||||
|
&self,
|
||||||
|
index: usize,
|
||||||
|
bind_group: BindGroupId,
|
||||||
|
dynamic_indices: &[u32],
|
||||||
|
) -> bool {
|
||||||
|
if let Some(current_bind_group) = self.bind_groups.get(index) {
|
||||||
|
current_bind_group.0 == Some(bind_group) && dynamic_indices == current_bind_group.1
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the vertex `buffer` as bound to the `index`.
|
||||||
|
fn set_vertex_buffer(&mut self, index: usize, buffer_slice: BufferSlice) {
|
||||||
|
self.vertex_buffers[index] = Some(self.buffer_slice_key(&buffer_slice));
|
||||||
|
self.stores_state = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks, whether the vertex `buffer` is already bound to the `index`.
|
||||||
|
fn is_vertex_buffer_set(&self, index: usize, buffer_slice: &BufferSlice) -> bool {
|
||||||
|
if let Some(current) = self.vertex_buffers.get(index) {
|
||||||
|
*current == Some(self.buffer_slice_key(buffer_slice))
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the value used for checking whether `BufferSlice`s are equivalent.
|
||||||
|
fn buffer_slice_key(&self, buffer_slice: &BufferSlice) -> BufferSliceKey {
|
||||||
|
(
|
||||||
|
buffer_slice.id(),
|
||||||
|
buffer_slice.offset(),
|
||||||
|
buffer_slice.size(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the index `buffer` as bound.
|
||||||
|
fn set_index_buffer(&mut self, buffer_slice: &BufferSlice, index_format: IndexFormat) {
|
||||||
|
self.index_buffer = Some((self.buffer_slice_key(buffer_slice), index_format));
|
||||||
|
self.stores_state = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks, whether the index `buffer` is already bound.
|
||||||
|
fn is_index_buffer_set(&self, buffer: &BufferSlice, index_format: IndexFormat) -> bool {
|
||||||
|
self.index_buffer == Some((self.buffer_slice_key(buffer), index_format))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets tracking state
|
||||||
|
pub fn reset_tracking(&mut self) {
|
||||||
|
if !self.stores_state {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.pipeline = None;
|
||||||
|
self.bind_groups.iter_mut().for_each(|val| {
|
||||||
|
val.0 = None;
|
||||||
|
val.1.clear();
|
||||||
|
});
|
||||||
|
self.vertex_buffers.iter_mut().for_each(|val| {
|
||||||
|
*val = None;
|
||||||
|
});
|
||||||
|
self.index_buffer = None;
|
||||||
|
self.stores_state = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`RenderPass`], which tracks the current pipeline state to skip redundant operations.
|
||||||
|
///
|
||||||
|
/// It is used to set the current [`RenderPipeline`], [`BindGroup`]s and [`Buffer`]s.
|
||||||
|
/// After all requirements are specified, draw calls can be issued.
|
||||||
|
pub struct TrackedRenderPass<'a> {
|
||||||
|
pass: RenderPass<'a>,
|
||||||
|
state: DrawState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TrackedRenderPass<'a> {
|
||||||
|
/// Tracks the supplied render pass.
|
||||||
|
pub fn new(device: &RenderDevice, pass: RenderPass<'a>) -> Self {
|
||||||
|
let limits = device.limits();
|
||||||
|
let max_bind_groups = limits.max_bind_groups as usize;
|
||||||
|
let max_vertex_buffers = limits.max_vertex_buffers as usize;
|
||||||
|
Self {
|
||||||
|
state: DrawState {
|
||||||
|
bind_groups: vec![(None, Vec::new()); max_bind_groups],
|
||||||
|
vertex_buffers: vec![None; max_vertex_buffers],
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
pass,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the wgpu [`RenderPass`].
|
||||||
|
///
|
||||||
|
/// Function invalidates internal tracking state,
|
||||||
|
/// some redundant pipeline operations may not be skipped.
|
||||||
|
pub fn wgpu_pass(&mut self) -> &mut RenderPass<'a> {
|
||||||
|
self.state.reset_tracking();
|
||||||
|
&mut self.pass
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the active [`RenderPipeline`].
|
||||||
|
///
|
||||||
|
/// Subsequent draw calls will exhibit the behavior defined by the `pipeline`.
|
||||||
|
pub fn set_render_pipeline(&mut self, pipeline: &'a RenderPipeline) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!("set pipeline: {:?}", pipeline);
|
||||||
|
if self.state.is_pipeline_set(pipeline.id()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.pass.set_pipeline(pipeline);
|
||||||
|
self.state.set_pipeline(pipeline.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the active bind group for a given bind group index. The bind group layout
|
||||||
|
/// in the active pipeline when any `draw()` function is called must match the layout of
|
||||||
|
/// this bind group.
|
||||||
|
///
|
||||||
|
/// If the bind group have dynamic offsets, provide them in binding order.
|
||||||
|
/// These offsets have to be aligned to [`WgpuLimits::min_uniform_buffer_offset_alignment`](crate::settings::WgpuLimits::min_uniform_buffer_offset_alignment)
|
||||||
|
/// or [`WgpuLimits::min_storage_buffer_offset_alignment`](crate::settings::WgpuLimits::min_storage_buffer_offset_alignment) appropriately.
|
||||||
|
pub fn set_bind_group(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
bind_group: &'a BindGroup,
|
||||||
|
dynamic_uniform_indices: &[u32],
|
||||||
|
) {
|
||||||
|
if self
|
||||||
|
.state
|
||||||
|
.is_bind_group_set(index, bind_group.id(), dynamic_uniform_indices)
|
||||||
|
{
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"set bind_group {} (already set): {:?} ({:?})",
|
||||||
|
index,
|
||||||
|
bind_group,
|
||||||
|
dynamic_uniform_indices
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"set bind_group {}: {:?} ({:?})",
|
||||||
|
index,
|
||||||
|
bind_group,
|
||||||
|
dynamic_uniform_indices
|
||||||
|
);
|
||||||
|
|
||||||
|
self.pass
|
||||||
|
.set_bind_group(index as u32, bind_group, dynamic_uniform_indices);
|
||||||
|
self.state
|
||||||
|
.set_bind_group(index, bind_group.id(), dynamic_uniform_indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assign a vertex buffer to a slot.
|
||||||
|
///
|
||||||
|
/// Subsequent calls to [`draw`] and [`draw_indexed`] on this
|
||||||
|
/// [`TrackedRenderPass`] will use `buffer` as one of the source vertex buffers.
|
||||||
|
///
|
||||||
|
/// The `slot_index` refers to the index of the matching descriptor in
|
||||||
|
/// [`VertexState::buffers`](crate::render_resource::VertexState::buffers).
|
||||||
|
///
|
||||||
|
/// [`draw`]: TrackedRenderPass::draw
|
||||||
|
/// [`draw_indexed`]: TrackedRenderPass::draw_indexed
|
||||||
|
pub fn set_vertex_buffer(&mut self, slot_index: usize, buffer_slice: BufferSlice<'a>) {
|
||||||
|
if self.state.is_vertex_buffer_set(slot_index, &buffer_slice) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"set vertex buffer {} (already set): {:?} (offset = {}, size = {})",
|
||||||
|
slot_index,
|
||||||
|
buffer_slice.id(),
|
||||||
|
buffer_slice.offset(),
|
||||||
|
buffer_slice.size(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"set vertex buffer {}: {:?} (offset = {}, size = {})",
|
||||||
|
slot_index,
|
||||||
|
buffer_slice.id(),
|
||||||
|
buffer_slice.offset(),
|
||||||
|
buffer_slice.size(),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.pass
|
||||||
|
.set_vertex_buffer(slot_index as u32, *buffer_slice);
|
||||||
|
self.state.set_vertex_buffer(slot_index, buffer_slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the active index buffer.
|
||||||
|
///
|
||||||
|
/// Subsequent calls to [`TrackedRenderPass::draw_indexed`] will use the buffer referenced by
|
||||||
|
/// `buffer_slice` as the source index buffer.
|
||||||
|
pub fn set_index_buffer(&mut self, buffer_slice: BufferSlice<'a>, index_format: IndexFormat) {
|
||||||
|
let already_set = self.state.is_index_buffer_set(&buffer_slice, index_format);
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"set index buffer{}: {:?} (offset = {}, size = {})",
|
||||||
|
if already_set { " (already set)" } else { "" },
|
||||||
|
buffer_slice.id(),
|
||||||
|
buffer_slice.offset(),
|
||||||
|
buffer_slice.size(),
|
||||||
|
);
|
||||||
|
if already_set {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.pass.set_index_buffer(*buffer_slice, index_format);
|
||||||
|
self.state.set_index_buffer(&buffer_slice, index_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws primitives from the active vertex buffer(s).
|
||||||
|
///
|
||||||
|
/// The active vertex buffer(s) can be set with [`TrackedRenderPass::set_vertex_buffer`].
|
||||||
|
pub fn draw(&mut self, vertices: Range<u32>, instances: Range<u32>) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!("draw: {:?} {:?}", vertices, instances);
|
||||||
|
self.pass.draw(vertices, instances);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws indexed primitives using the active index buffer and the active vertex buffer(s).
|
||||||
|
///
|
||||||
|
/// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the
|
||||||
|
/// active vertex buffer(s) can be set with [`TrackedRenderPass::set_vertex_buffer`].
|
||||||
|
pub fn draw_indexed(&mut self, indices: Range<u32>, base_vertex: i32, instances: Range<u32>) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"draw indexed: {:?} {} {:?}",
|
||||||
|
indices,
|
||||||
|
base_vertex,
|
||||||
|
instances
|
||||||
|
);
|
||||||
|
self.pass.draw_indexed(indices, base_vertex, instances);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws primitives from the active vertex buffer(s) based on the contents of the
|
||||||
|
/// `indirect_buffer`.
|
||||||
|
///
|
||||||
|
/// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`].
|
||||||
|
///
|
||||||
|
/// The structure expected in `indirect_buffer` is the following:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[repr(C)]
|
||||||
|
/// struct DrawIndirect {
|
||||||
|
/// vertex_count: u32, // The number of vertices to draw.
|
||||||
|
/// instance_count: u32, // The number of instances to draw.
|
||||||
|
/// first_vertex: u32, // The Index of the first vertex to draw.
|
||||||
|
/// first_instance: u32, // The instance ID of the first instance to draw.
|
||||||
|
/// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled.
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn draw_indirect(&mut self, indirect_buffer: &'a Buffer, indirect_offset: u64) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!("draw indirect: {:?} {}", indirect_buffer, indirect_offset);
|
||||||
|
self.pass.draw_indirect(indirect_buffer, indirect_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws indexed primitives using the active index buffer and the active vertex buffers,
|
||||||
|
/// based on the contents of the `indirect_buffer`.
|
||||||
|
///
|
||||||
|
/// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the
|
||||||
|
/// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`].
|
||||||
|
///
|
||||||
|
/// The structure expected in `indirect_buffer` is the following:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[repr(C)]
|
||||||
|
/// struct DrawIndexedIndirect {
|
||||||
|
/// vertex_count: u32, // The number of vertices to draw.
|
||||||
|
/// instance_count: u32, // The number of instances to draw.
|
||||||
|
/// first_index: u32, // The base index within the index buffer.
|
||||||
|
/// vertex_offset: i32, // The value added to the vertex index before indexing into the vertex buffer.
|
||||||
|
/// first_instance: u32, // The instance ID of the first instance to draw.
|
||||||
|
/// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled.
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn draw_indexed_indirect(&mut self, indirect_buffer: &'a Buffer, indirect_offset: u64) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"draw indexed indirect: {:?} {}",
|
||||||
|
indirect_buffer,
|
||||||
|
indirect_offset
|
||||||
|
);
|
||||||
|
self.pass
|
||||||
|
.draw_indexed_indirect(indirect_buffer, indirect_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches multiple draw calls from the active vertex buffer(s) based on the contents of the
|
||||||
|
/// `indirect_buffer`.`count` draw calls are issued.
|
||||||
|
///
|
||||||
|
/// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`].
|
||||||
|
///
|
||||||
|
/// `indirect_buffer` should contain `count` tightly packed elements of the following structure:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[repr(C)]
|
||||||
|
/// struct DrawIndirect {
|
||||||
|
/// vertex_count: u32, // The number of vertices to draw.
|
||||||
|
/// instance_count: u32, // The number of instances to draw.
|
||||||
|
/// first_vertex: u32, // The Index of the first vertex to draw.
|
||||||
|
/// first_instance: u32, // The instance ID of the first instance to draw.
|
||||||
|
/// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled.
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn multi_draw_indirect(
|
||||||
|
&mut self,
|
||||||
|
indirect_buffer: &'a Buffer,
|
||||||
|
indirect_offset: u64,
|
||||||
|
count: u32,
|
||||||
|
) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"multi draw indirect: {:?} {}, {}x",
|
||||||
|
indirect_buffer,
|
||||||
|
indirect_offset,
|
||||||
|
count
|
||||||
|
);
|
||||||
|
self.pass
|
||||||
|
.multi_draw_indirect(indirect_buffer, indirect_offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches multiple draw calls from the active vertex buffer(s) based on the contents of
|
||||||
|
/// the `indirect_buffer`.
|
||||||
|
/// The count buffer is read to determine how many draws to issue.
|
||||||
|
///
|
||||||
|
/// The indirect buffer must be long enough to account for `max_count` draws, however only
|
||||||
|
/// `count` elements will be read, where `count` is the value read from `count_buffer` capped
|
||||||
|
/// at `max_count`.
|
||||||
|
///
|
||||||
|
/// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`].
|
||||||
|
///
|
||||||
|
/// `indirect_buffer` should contain `count` tightly packed elements of the following structure:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[repr(C)]
|
||||||
|
/// struct DrawIndirect {
|
||||||
|
/// vertex_count: u32, // The number of vertices to draw.
|
||||||
|
/// instance_count: u32, // The number of instances to draw.
|
||||||
|
/// first_vertex: u32, // The Index of the first vertex to draw.
|
||||||
|
/// first_instance: u32, // The instance ID of the first instance to draw.
|
||||||
|
/// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled.
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn multi_draw_indirect_count(
|
||||||
|
&mut self,
|
||||||
|
indirect_buffer: &'a Buffer,
|
||||||
|
indirect_offset: u64,
|
||||||
|
count_buffer: &'a Buffer,
|
||||||
|
count_offset: u64,
|
||||||
|
max_count: u32,
|
||||||
|
) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"multi draw indirect count: {:?} {}, ({:?} {})x, max {}x",
|
||||||
|
indirect_buffer,
|
||||||
|
indirect_offset,
|
||||||
|
count_buffer,
|
||||||
|
count_offset,
|
||||||
|
max_count
|
||||||
|
);
|
||||||
|
self.pass.multi_draw_indirect_count(
|
||||||
|
indirect_buffer,
|
||||||
|
indirect_offset,
|
||||||
|
count_buffer,
|
||||||
|
count_offset,
|
||||||
|
max_count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches multiple draw calls from the active index buffer and the active vertex buffers,
|
||||||
|
/// based on the contents of the `indirect_buffer`. `count` draw calls are issued.
|
||||||
|
///
|
||||||
|
/// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the
|
||||||
|
/// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`].
|
||||||
|
///
|
||||||
|
/// `indirect_buffer` should contain `count` tightly packed elements of the following structure:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[repr(C)]
|
||||||
|
/// struct DrawIndexedIndirect {
|
||||||
|
/// vertex_count: u32, // The number of vertices to draw.
|
||||||
|
/// instance_count: u32, // The number of instances to draw.
|
||||||
|
/// first_index: u32, // The base index within the index buffer.
|
||||||
|
/// vertex_offset: i32, // The value added to the vertex index before indexing into the vertex buffer.
|
||||||
|
/// first_instance: u32, // The instance ID of the first instance to draw.
|
||||||
|
/// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled.
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn multi_draw_indexed_indirect(
|
||||||
|
&mut self,
|
||||||
|
indirect_buffer: &'a Buffer,
|
||||||
|
indirect_offset: u64,
|
||||||
|
count: u32,
|
||||||
|
) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"multi draw indexed indirect: {:?} {}, {}x",
|
||||||
|
indirect_buffer,
|
||||||
|
indirect_offset,
|
||||||
|
count
|
||||||
|
);
|
||||||
|
self.pass
|
||||||
|
.multi_draw_indexed_indirect(indirect_buffer, indirect_offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches multiple draw calls from the active index buffer and the active vertex buffers,
|
||||||
|
/// based on the contents of the `indirect_buffer`.
|
||||||
|
/// The count buffer is read to determine how many draws to issue.
|
||||||
|
///
|
||||||
|
/// The indirect buffer must be long enough to account for `max_count` draws, however only
|
||||||
|
/// `count` elements will be read, where `count` is the value read from `count_buffer` capped
|
||||||
|
/// at `max_count`.
|
||||||
|
///
|
||||||
|
/// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the
|
||||||
|
/// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`].
|
||||||
|
///
|
||||||
|
/// `indirect_buffer` should contain `count` tightly packed elements of the following structure:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[repr(C)]
|
||||||
|
/// struct DrawIndexedIndirect {
|
||||||
|
/// vertex_count: u32, // The number of vertices to draw.
|
||||||
|
/// instance_count: u32, // The number of instances to draw.
|
||||||
|
/// first_index: u32, // The base index within the index buffer.
|
||||||
|
/// vertex_offset: i32, // The value added to the vertex index before indexing into the vertex buffer.
|
||||||
|
/// first_instance: u32, // The instance ID of the first instance to draw.
|
||||||
|
/// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled.
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn multi_draw_indexed_indirect_count(
|
||||||
|
&mut self,
|
||||||
|
indirect_buffer: &'a Buffer,
|
||||||
|
indirect_offset: u64,
|
||||||
|
count_buffer: &'a Buffer,
|
||||||
|
count_offset: u64,
|
||||||
|
max_count: u32,
|
||||||
|
) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"multi draw indexed indirect count: {:?} {}, ({:?} {})x, max {}x",
|
||||||
|
indirect_buffer,
|
||||||
|
indirect_offset,
|
||||||
|
count_buffer,
|
||||||
|
count_offset,
|
||||||
|
max_count
|
||||||
|
);
|
||||||
|
self.pass.multi_draw_indexed_indirect_count(
|
||||||
|
indirect_buffer,
|
||||||
|
indirect_offset,
|
||||||
|
count_buffer,
|
||||||
|
count_offset,
|
||||||
|
max_count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the stencil reference.
|
||||||
|
///
|
||||||
|
/// Subsequent stencil tests will test against this value.
|
||||||
|
pub fn set_stencil_reference(&mut self, reference: u32) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!("set stencil reference: {}", reference);
|
||||||
|
self.pass.set_stencil_reference(reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the scissor region.
|
||||||
|
///
|
||||||
|
/// Subsequent draw calls will discard any fragments that fall outside this region.
|
||||||
|
pub fn set_scissor_rect(&mut self, x: u32, y: u32, width: u32, height: u32) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!("set_scissor_rect: {} {} {} {}", x, y, width, height);
|
||||||
|
self.pass.set_scissor_rect(x, y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set push constant data.
|
||||||
|
///
|
||||||
|
/// `Features::PUSH_CONSTANTS` must be enabled on the device in order to call these functions.
|
||||||
|
pub fn set_push_constants(&mut self, stages: ShaderStages, offset: u32, data: &[u8]) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"set push constants: {:?} offset: {} data.len: {}",
|
||||||
|
stages,
|
||||||
|
offset,
|
||||||
|
data.len()
|
||||||
|
);
|
||||||
|
self.pass.set_push_constants(stages, offset, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the rendering viewport.
|
||||||
|
///
|
||||||
|
/// Subsequent draw calls will be projected into that viewport.
|
||||||
|
pub fn set_viewport(
|
||||||
|
&mut self,
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
min_depth: f32,
|
||||||
|
max_depth: f32,
|
||||||
|
) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!(
|
||||||
|
"set viewport: {} {} {} {} {} {}",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
min_depth,
|
||||||
|
max_depth
|
||||||
|
);
|
||||||
|
self.pass
|
||||||
|
.set_viewport(x, y, width, height, min_depth, max_depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the rendering viewport to the given camera [`Viewport`].
|
||||||
|
///
|
||||||
|
/// Subsequent draw calls will be projected into that viewport.
|
||||||
|
pub fn set_camera_viewport(&mut self, viewport: &Viewport) {
|
||||||
|
self.set_viewport(
|
||||||
|
viewport.physical_position.x as f32,
|
||||||
|
viewport.physical_position.y as f32,
|
||||||
|
viewport.physical_size.x as f32,
|
||||||
|
viewport.physical_size.y as f32,
|
||||||
|
viewport.depth.start,
|
||||||
|
viewport.depth.end,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a single debug marker.
|
||||||
|
///
|
||||||
|
/// This is a GPU debugging feature. This has no effect on the rendering itself.
|
||||||
|
pub fn insert_debug_marker(&mut self, label: &str) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!("insert debug marker: {}", label);
|
||||||
|
self.pass.insert_debug_marker(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a new debug group.
|
||||||
|
///
|
||||||
|
/// Push a new debug group over the internal stack. Subsequent render commands and debug
|
||||||
|
/// markers are grouped into this new group, until [`pop_debug_group`] is called.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # fn example(mut pass: bevy_render::render_phase::TrackedRenderPass<'static>) {
|
||||||
|
/// pass.push_debug_group("Render the car");
|
||||||
|
/// // [setup pipeline etc...]
|
||||||
|
/// pass.draw(0..64, 0..1);
|
||||||
|
/// pass.pop_debug_group();
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note that [`push_debug_group`] and [`pop_debug_group`] must always be called in pairs.
|
||||||
|
///
|
||||||
|
/// This is a GPU debugging feature. This has no effect on the rendering itself.
|
||||||
|
///
|
||||||
|
/// [`push_debug_group`]: TrackedRenderPass::push_debug_group
|
||||||
|
/// [`pop_debug_group`]: TrackedRenderPass::pop_debug_group
|
||||||
|
pub fn push_debug_group(&mut self, label: &str) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!("push_debug_group marker: {}", label);
|
||||||
|
self.pass.push_debug_group(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End the current debug group.
|
||||||
|
///
|
||||||
|
/// Subsequent render commands and debug markers are not grouped anymore in
|
||||||
|
/// this group, but in the previous one (if any) or the default top-level one
|
||||||
|
/// if the debug group was the last one on the stack.
|
||||||
|
///
|
||||||
|
/// Note that [`push_debug_group`] and [`pop_debug_group`] must always be called in pairs.
|
||||||
|
///
|
||||||
|
/// This is a GPU debugging feature. This has no effect on the rendering itself.
|
||||||
|
///
|
||||||
|
/// [`push_debug_group`]: TrackedRenderPass::push_debug_group
|
||||||
|
/// [`pop_debug_group`]: TrackedRenderPass::pop_debug_group
|
||||||
|
pub fn pop_debug_group(&mut self) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!("pop_debug_group");
|
||||||
|
self.pass.pop_debug_group();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the blend color as used by some of the blending modes.
|
||||||
|
///
|
||||||
|
/// Subsequent blending tests will test against this value.
|
||||||
|
pub fn set_blend_constant(&mut self, color: LinearRgba) {
|
||||||
|
#[cfg(feature = "detailed_trace")]
|
||||||
|
trace!("set blend constant: {:?}", color);
|
||||||
|
self.pass.set_blend_constant(wgpu::Color::from(color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteTimestamp for TrackedRenderPass<'_> {
|
||||||
|
fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) {
|
||||||
|
self.pass.write_timestamp(query_set, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WritePipelineStatistics for TrackedRenderPass<'_> {
|
||||||
|
fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) {
|
||||||
|
self.pass.begin_pipeline_statistics_query(query_set, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_pipeline_statistics_query(&mut self) {
|
||||||
|
self.pass.end_pipeline_statistics_query();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pass for TrackedRenderPass<'_> {
|
||||||
|
const KIND: PassKind = PassKind::Render;
|
||||||
|
}
|
||||||
1911
third_party/bevy_render/src/render_phase/mod.rs
vendored
Normal file
1911
third_party/bevy_render/src/render_phase/mod.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
39
third_party/bevy_render/src/render_phase/rangefinder.rs
vendored
Normal file
39
third_party/bevy_render/src/render_phase/rangefinder.rs
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use bevy_math::{Affine3A, Mat4, Vec3, Vec4};
|
||||||
|
|
||||||
|
/// A distance calculator for the draw order of [`PhaseItem`](crate::render_phase::PhaseItem)s.
|
||||||
|
pub struct ViewRangefinder3d {
|
||||||
|
view_from_world_row_2: Vec4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewRangefinder3d {
|
||||||
|
/// Creates a 3D rangefinder for a view matrix.
|
||||||
|
pub fn from_world_from_view(world_from_view: &Affine3A) -> ViewRangefinder3d {
|
||||||
|
let view_from_world = world_from_view.inverse();
|
||||||
|
|
||||||
|
ViewRangefinder3d {
|
||||||
|
view_from_world_row_2: Mat4::from(view_from_world).row(2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the distance, or view-space `Z` value, for the given world-space `position`.
|
||||||
|
#[inline]
|
||||||
|
pub fn distance(&self, position: &Vec3) -> f32 {
|
||||||
|
// NOTE: row 2 of the inverse view matrix dotted with the world-space position
|
||||||
|
// gives the z component of the point in view-space
|
||||||
|
self.view_from_world_row_2.dot(position.extend(1.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::ViewRangefinder3d;
|
||||||
|
use bevy_math::{Affine3A, Vec3};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn distance() {
|
||||||
|
let view_matrix = Affine3A::from_translation(Vec3::new(0.0, 0.0, -1.0));
|
||||||
|
let rangefinder = ViewRangefinder3d::from_world_from_view(&view_matrix);
|
||||||
|
assert_eq!(rangefinder.distance(&Vec3::new(0., 0., 0.)), 1.0);
|
||||||
|
assert_eq!(rangefinder.distance(&Vec3::new(0., 0., 1.)), 2.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
157
third_party/bevy_render/src/render_resource/batched_uniform_buffer.rs
vendored
Normal file
157
third_party/bevy_render/src/render_resource/batched_uniform_buffer.rs
vendored
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
use super::{GpuArrayBufferIndex, GpuArrayBufferable};
|
||||||
|
use crate::{
|
||||||
|
render_resource::DynamicUniformBuffer,
|
||||||
|
renderer::{RenderDevice, RenderQueue},
|
||||||
|
};
|
||||||
|
use core::{marker::PhantomData, num::NonZero};
|
||||||
|
use encase::{
|
||||||
|
private::{ArrayMetadata, BufferMut, Metadata, RuntimeSizedArray, WriteInto, Writer},
|
||||||
|
ShaderType,
|
||||||
|
};
|
||||||
|
use nonmax::NonMaxU32;
|
||||||
|
use wgpu::{BindingResource, Limits};
|
||||||
|
|
||||||
|
// 1MB else we will make really large arrays on macOS which reports very large
|
||||||
|
// `max_uniform_buffer_binding_size`. On macOS this ends up being the minimum
|
||||||
|
// size of the uniform buffer as well as the size of each chunk of data at a
|
||||||
|
// dynamic offset.
|
||||||
|
#[cfg(any(
|
||||||
|
not(feature = "webgl"),
|
||||||
|
not(target_arch = "wasm32"),
|
||||||
|
feature = "webgpu"
|
||||||
|
))]
|
||||||
|
const MAX_REASONABLE_UNIFORM_BUFFER_BINDING_SIZE: u32 = 1 << 20;
|
||||||
|
|
||||||
|
// WebGL2 quirk: using uniform buffers larger than 4KB will cause extremely
|
||||||
|
// long shader compilation times, so the limit needs to be lower on WebGL2.
|
||||||
|
// This is due to older shader compilers/GPUs that don't support dynamically
|
||||||
|
// indexing uniform buffers, and instead emulate it with large switch statements
|
||||||
|
// over buffer indices that take a long time to compile.
|
||||||
|
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
|
||||||
|
const MAX_REASONABLE_UNIFORM_BUFFER_BINDING_SIZE: u32 = 1 << 12;
|
||||||
|
|
||||||
|
/// Similar to [`DynamicUniformBuffer`], except every N elements (depending on size)
|
||||||
|
/// are grouped into a batch as an `array<T, N>` in WGSL.
|
||||||
|
///
|
||||||
|
/// This reduces the number of rebindings required due to having to pass dynamic
|
||||||
|
/// offsets to bind group commands, and if indices into the array can be passed
|
||||||
|
/// in via other means, it enables batching of draw commands.
|
||||||
|
pub struct BatchedUniformBuffer<T: GpuArrayBufferable> {
|
||||||
|
// Batches of fixed-size arrays of T are written to this buffer so that
|
||||||
|
// each batch in a fixed-size array can be bound at a dynamic offset.
|
||||||
|
uniforms: DynamicUniformBuffer<MaxCapacityArray<Vec<T>>>,
|
||||||
|
// A batch of T are gathered into this `MaxCapacityArray` until it is full,
|
||||||
|
// then it is written into the `DynamicUniformBuffer`, cleared, and new T
|
||||||
|
// are gathered here, and so on for each batch.
|
||||||
|
temp: MaxCapacityArray<Vec<T>>,
|
||||||
|
current_offset: u32,
|
||||||
|
dynamic_offset_alignment: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: GpuArrayBufferable> BatchedUniformBuffer<T> {
|
||||||
|
pub fn batch_size(limits: &Limits) -> usize {
|
||||||
|
(limits
|
||||||
|
.max_uniform_buffer_binding_size
|
||||||
|
.min(MAX_REASONABLE_UNIFORM_BUFFER_BINDING_SIZE) as u64
|
||||||
|
/ T::min_size().get()) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(limits: &Limits) -> Self {
|
||||||
|
let capacity = Self::batch_size(limits);
|
||||||
|
let alignment = limits.min_uniform_buffer_offset_alignment;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
uniforms: DynamicUniformBuffer::new_with_alignment(alignment as u64),
|
||||||
|
temp: MaxCapacityArray(Vec::with_capacity(capacity), capacity),
|
||||||
|
current_offset: 0,
|
||||||
|
dynamic_offset_alignment: alignment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn size(&self) -> NonZero<u64> {
|
||||||
|
self.temp.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.uniforms.clear();
|
||||||
|
self.current_offset = 0;
|
||||||
|
self.temp.0.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, component: T) -> GpuArrayBufferIndex<T> {
|
||||||
|
let result = GpuArrayBufferIndex {
|
||||||
|
index: self.temp.0.len() as u32,
|
||||||
|
dynamic_offset: NonMaxU32::new(self.current_offset),
|
||||||
|
element_type: PhantomData,
|
||||||
|
};
|
||||||
|
self.temp.0.push(component);
|
||||||
|
if self.temp.0.len() == self.temp.1 {
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(&mut self) {
|
||||||
|
self.uniforms.push(&self.temp);
|
||||||
|
|
||||||
|
self.current_offset +=
|
||||||
|
align_to_next(self.temp.size().get(), self.dynamic_offset_alignment as u64) as u32;
|
||||||
|
|
||||||
|
self.temp.0.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) {
|
||||||
|
if !self.temp.0.is_empty() {
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
self.uniforms.write_buffer(device, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn binding(&self) -> Option<BindingResource<'_>> {
|
||||||
|
let mut binding = self.uniforms.binding();
|
||||||
|
if let Some(BindingResource::Buffer(binding)) = &mut binding {
|
||||||
|
// MaxCapacityArray is runtime-sized so can't use T::min_size()
|
||||||
|
binding.size = Some(self.size());
|
||||||
|
}
|
||||||
|
binding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn align_to_next(value: u64, alignment: u64) -> u64 {
|
||||||
|
debug_assert!(alignment.is_power_of_two());
|
||||||
|
((value - 1) | (alignment - 1)) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// MaxCapacityArray was implemented by Teodor Tanasoaia for encase. It was
|
||||||
|
// copied here as it was not yet included in an encase release and it is
|
||||||
|
// unclear if it is the correct long-term solution for encase.
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
struct MaxCapacityArray<T>(T, usize);
|
||||||
|
|
||||||
|
impl<T> ShaderType for MaxCapacityArray<T>
|
||||||
|
where
|
||||||
|
T: ShaderType<ExtraMetadata = ArrayMetadata>,
|
||||||
|
{
|
||||||
|
type ExtraMetadata = ArrayMetadata;
|
||||||
|
|
||||||
|
const METADATA: Metadata<Self::ExtraMetadata> = T::METADATA;
|
||||||
|
|
||||||
|
fn size(&self) -> NonZero<u64> {
|
||||||
|
Self::METADATA.stride().mul(self.1.max(1) as u64).0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> WriteInto for MaxCapacityArray<T>
|
||||||
|
where
|
||||||
|
T: WriteInto + RuntimeSizedArray,
|
||||||
|
{
|
||||||
|
fn write_into<B: BufferMut>(&self, writer: &mut Writer<B>) {
|
||||||
|
debug_assert!(self.0.len() <= self.1);
|
||||||
|
self.0.write_into(writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
740
third_party/bevy_render/src/render_resource/bind_group.rs
vendored
Normal file
740
third_party/bevy_render/src/render_resource/bind_group.rs
vendored
Normal file
@ -0,0 +1,740 @@
|
|||||||
|
use crate::{
|
||||||
|
define_atomic_id,
|
||||||
|
render_asset::RenderAssets,
|
||||||
|
render_resource::{BindGroupLayout, Buffer, PipelineCache, Sampler, TextureView},
|
||||||
|
renderer::{RenderDevice, WgpuWrapper},
|
||||||
|
texture::GpuImage,
|
||||||
|
};
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::system::{SystemParam, SystemParamItem};
|
||||||
|
use bevy_render::render_resource::BindGroupLayoutDescriptor;
|
||||||
|
pub use bevy_render_macros::AsBindGroup;
|
||||||
|
use core::ops::Deref;
|
||||||
|
use encase::ShaderType;
|
||||||
|
use thiserror::Error;
|
||||||
|
use wgpu::{
|
||||||
|
BindGroupEntry, BindGroupLayoutEntry, BindingResource, SamplerBindingType, TextureViewDimension,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{BindlessDescriptor, BindlessSlabResourceLimit};
|
||||||
|
|
||||||
|
define_atomic_id!(BindGroupId);
|
||||||
|
|
||||||
|
/// Bind groups are responsible for binding render resources (e.g. buffers, textures, samplers)
|
||||||
|
/// to a [`TrackedRenderPass`](crate::render_phase::TrackedRenderPass).
|
||||||
|
/// This makes them accessible in the pipeline (shaders) as uniforms.
|
||||||
|
///
|
||||||
|
/// This is a lightweight thread-safe wrapper around wgpu's own [`BindGroup`](wgpu::BindGroup),
|
||||||
|
/// which can be cloned as needed to workaround lifetime management issues. It may be converted
|
||||||
|
/// from and dereferences to wgpu's [`BindGroup`](wgpu::BindGroup).
|
||||||
|
///
|
||||||
|
/// Can be created via [`RenderDevice::create_bind_group`](RenderDevice::create_bind_group).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BindGroup {
|
||||||
|
id: BindGroupId,
|
||||||
|
value: WgpuWrapper<wgpu::BindGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindGroup {
|
||||||
|
/// Returns the [`BindGroupId`] representing the unique ID of the bind group.
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> BindGroupId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BindGroup {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for BindGroup {}
|
||||||
|
|
||||||
|
impl core::hash::Hash for BindGroup {
|
||||||
|
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.0.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wgpu::BindGroup> for BindGroup {
|
||||||
|
fn from(value: wgpu::BindGroup) -> Self {
|
||||||
|
BindGroup {
|
||||||
|
id: BindGroupId::new(),
|
||||||
|
value: WgpuWrapper::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a BindGroup> for Option<&'a wgpu::BindGroup> {
|
||||||
|
fn from(value: &'a BindGroup) -> Self {
|
||||||
|
Some(value.deref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a mut BindGroup> for Option<&'a wgpu::BindGroup> {
|
||||||
|
fn from(value: &'a mut BindGroup) -> Self {
|
||||||
|
Some(&*value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for BindGroup {
|
||||||
|
type Target = wgpu::BindGroup;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a value to a [`BindGroup`] with a given [`BindGroupLayout`], which can then be used in Bevy shaders.
|
||||||
|
/// This trait can be derived (and generally should be). Read on for details and examples.
|
||||||
|
///
|
||||||
|
/// This is an opinionated trait that is intended to make it easy to generically
|
||||||
|
/// convert a type into a [`BindGroup`]. It provides access to specific render resources,
|
||||||
|
/// such as [`RenderAssets<GpuImage>`] and [`crate::texture::FallbackImage`]. If a type has a [`Handle<Image>`](bevy_asset::Handle),
|
||||||
|
/// these can be used to retrieve the corresponding [`Texture`](crate::render_resource::Texture) resource.
|
||||||
|
///
|
||||||
|
/// [`AsBindGroup::as_bind_group`] is intended to be called once, then the result cached somewhere. It is generally
|
||||||
|
/// ok to do "expensive" work here, such as creating a [`Buffer`] for a uniform.
|
||||||
|
///
|
||||||
|
/// If for some reason a [`BindGroup`] cannot be created yet (for example, the [`Texture`](crate::render_resource::Texture)
|
||||||
|
/// for an [`Image`](bevy_image::Image) hasn't loaded yet), just return [`AsBindGroupError::RetryNextUpdate`], which signals that the caller
|
||||||
|
/// should retry again later.
|
||||||
|
///
|
||||||
|
/// # Deriving
|
||||||
|
///
|
||||||
|
/// This trait can be derived. Field attributes like `uniform` and `texture` are used to define which fields should be bindings,
|
||||||
|
/// what their binding type is, and what index they should be bound at:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_render::render_resource::*;
|
||||||
|
/// # use bevy_image::Image;
|
||||||
|
/// # use bevy_color::LinearRgba;
|
||||||
|
/// # use bevy_asset::Handle;
|
||||||
|
/// # use bevy_render::storage::ShaderStorageBuffer;
|
||||||
|
///
|
||||||
|
/// #[derive(AsBindGroup)]
|
||||||
|
/// struct CoolMaterial {
|
||||||
|
/// #[uniform(0)]
|
||||||
|
/// color: LinearRgba,
|
||||||
|
/// #[texture(1)]
|
||||||
|
/// #[sampler(2)]
|
||||||
|
/// color_texture: Handle<Image>,
|
||||||
|
/// #[storage(3, read_only)]
|
||||||
|
/// storage_buffer: Handle<ShaderStorageBuffer>,
|
||||||
|
/// #[storage(4, read_only, buffer)]
|
||||||
|
/// raw_buffer: Buffer,
|
||||||
|
/// #[storage_texture(5)]
|
||||||
|
/// storage_texture: Handle<Image>,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// In WGSL shaders, the binding would look like this:
|
||||||
|
///
|
||||||
|
/// ```wgsl
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> color: vec4<f32>;
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(1) var color_texture: texture_2d<f32>;
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(2) var color_sampler: sampler;
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(3) var<storage> storage_buffer: array<f32>;
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(4) var<storage> raw_buffer: array<f32>;
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(5) var storage_texture: texture_storage_2d<rgba8unorm, read_write>;
|
||||||
|
/// ```
|
||||||
|
/// Note that the "group" index is determined by the usage context. It is not defined in [`AsBindGroup`]. For example, in Bevy material bind groups
|
||||||
|
/// are generally bound to group 2.
|
||||||
|
///
|
||||||
|
/// The following field-level attributes are supported:
|
||||||
|
///
|
||||||
|
/// ## `uniform(BINDING_INDEX)`
|
||||||
|
///
|
||||||
|
/// * The field will be converted to a shader-compatible type using the [`ShaderType`] trait, written to a [`Buffer`], and bound as a uniform.
|
||||||
|
/// [`ShaderType`] is implemented for most math types already, such as [`f32`], [`Vec4`](bevy_math::Vec4), and
|
||||||
|
/// [`LinearRgba`](bevy_color::LinearRgba). It can also be derived for custom structs.
|
||||||
|
///
|
||||||
|
/// ## `texture(BINDING_INDEX, arguments)`
|
||||||
|
///
|
||||||
|
/// * This field's [`Handle<Image>`](bevy_asset::Handle) will be used to look up the matching [`Texture`](crate::render_resource::Texture)
|
||||||
|
/// GPU resource, which will be bound as a texture in shaders. The field will be assumed to implement [`Into<Option<Handle<Image>>>`]. In practice,
|
||||||
|
/// most fields should be a [`Handle<Image>`](bevy_asset::Handle) or [`Option<Handle<Image>>`]. If the value of an [`Option<Handle<Image>>`] is
|
||||||
|
/// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `sampler` binding attribute
|
||||||
|
/// (with a different binding index) if a binding of the sampler for the [`Image`](bevy_image::Image) is also required.
|
||||||
|
///
|
||||||
|
/// | Arguments | Values | Default |
|
||||||
|
/// |-----------------------|-------------------------------------------------------------------------|----------------------|
|
||||||
|
/// | `dimension` = "..." | `"1d"`, `"2d"`, `"2d_array"`, `"3d"`, `"cube"`, `"cube_array"` | `"2d"` |
|
||||||
|
/// | `sample_type` = "..." | `"float"`, `"depth"`, `"s_int"` or `"u_int"` | `"float"` |
|
||||||
|
/// | `filterable` = ... | `true`, `false` | `true` |
|
||||||
|
/// | `multisampled` = ... | `true`, `false` | `false` |
|
||||||
|
/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` |
|
||||||
|
///
|
||||||
|
/// ## `storage_texture(BINDING_INDEX, arguments)`
|
||||||
|
///
|
||||||
|
/// * This field's [`Handle<Image>`](bevy_asset::Handle) will be used to look up the matching [`Texture`](crate::render_resource::Texture)
|
||||||
|
/// GPU resource, which will be bound as a storage texture in shaders. The field will be assumed to implement [`Into<Option<Handle<Image>>>`]. In practice,
|
||||||
|
/// most fields should be a [`Handle<Image>`](bevy_asset::Handle) or [`Option<Handle<Image>>`]. If the value of an [`Option<Handle<Image>>`] is
|
||||||
|
/// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead.
|
||||||
|
///
|
||||||
|
/// | Arguments | Values | Default |
|
||||||
|
/// |------------------------|--------------------------------------------------------------------------------------------|---------------|
|
||||||
|
/// | `dimension` = "..." | `"1d"`, `"2d"`, `"2d_array"`, `"3d"`, `"cube"`, `"cube_array"` | `"2d"` |
|
||||||
|
/// | `image_format` = ... | any member of [`TextureFormat`](crate::render_resource::TextureFormat) | `Rgba8Unorm` |
|
||||||
|
/// | `access` = ... | any member of [`StorageTextureAccess`](crate::render_resource::StorageTextureAccess) | `ReadWrite` |
|
||||||
|
/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `compute` |
|
||||||
|
///
|
||||||
|
/// ## `sampler(BINDING_INDEX, arguments)`
|
||||||
|
///
|
||||||
|
/// * This field's [`Handle<Image>`](bevy_asset::Handle) will be used to look up the matching [`Sampler`] GPU
|
||||||
|
/// resource, which will be bound as a sampler in shaders. The field will be assumed to implement [`Into<Option<Handle<Image>>>`]. In practice,
|
||||||
|
/// most fields should be a [`Handle<Image>`](bevy_asset::Handle) or [`Option<Handle<Image>>`]. If the value of an [`Option<Handle<Image>>`] is
|
||||||
|
/// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `texture` binding attribute
|
||||||
|
/// (with a different binding index) if a binding of the texture for the [`Image`](bevy_image::Image) is also required.
|
||||||
|
///
|
||||||
|
/// | Arguments | Values | Default |
|
||||||
|
/// |------------------------|-------------------------------------------------------------------------|------------------------|
|
||||||
|
/// | `sampler_type` = "..." | `"filtering"`, `"non_filtering"`, `"comparison"`. | `"filtering"` |
|
||||||
|
/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` |
|
||||||
|
///
|
||||||
|
/// ## `storage(BINDING_INDEX, arguments)`
|
||||||
|
///
|
||||||
|
/// * The field's [`Handle<Storage>`](bevy_asset::Handle) will be used to look
|
||||||
|
/// up the matching [`Buffer`] GPU resource, which will be bound as a storage
|
||||||
|
/// buffer in shaders. If the `storage` attribute is used, the field is expected
|
||||||
|
/// a raw buffer, and the buffer will be bound as a storage buffer in shaders.
|
||||||
|
/// In bindless mode, `binding_array()` argument that specifies the binding
|
||||||
|
/// number of the resulting storage buffer binding array must be present.
|
||||||
|
///
|
||||||
|
/// | Arguments | Values | Default |
|
||||||
|
/// |------------------------|-------------------------------------------------------------------------|------------------------|
|
||||||
|
/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` |
|
||||||
|
/// | `read_only` | if present then value is true, otherwise false | `false` |
|
||||||
|
/// | `buffer` | if present then the field will be assumed to be a raw wgpu buffer | |
|
||||||
|
/// | `binding_array(...)` | the binding number of the binding array, for bindless mode | bindless mode disabled |
|
||||||
|
///
|
||||||
|
/// Note that fields without field-level binding attributes will be ignored.
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_render::{render_resource::AsBindGroup};
|
||||||
|
/// # use bevy_color::LinearRgba;
|
||||||
|
/// # use bevy_asset::Handle;
|
||||||
|
/// #[derive(AsBindGroup)]
|
||||||
|
/// struct CoolMaterial {
|
||||||
|
/// #[uniform(0)]
|
||||||
|
/// color: LinearRgba,
|
||||||
|
/// this_field_is_ignored: String,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// As mentioned above, [`Option<Handle<Image>>`] is also supported:
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_asset::Handle;
|
||||||
|
/// # use bevy_color::LinearRgba;
|
||||||
|
/// # use bevy_image::Image;
|
||||||
|
/// # use bevy_render::render_resource::AsBindGroup;
|
||||||
|
/// #[derive(AsBindGroup)]
|
||||||
|
/// struct CoolMaterial {
|
||||||
|
/// #[uniform(0)]
|
||||||
|
/// color: LinearRgba,
|
||||||
|
/// #[texture(1)]
|
||||||
|
/// #[sampler(2)]
|
||||||
|
/// color_texture: Option<Handle<Image>>,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// This is useful if you want a texture to be optional. When the value is [`None`], the [`crate::texture::FallbackImage`] will be used for the binding instead, which defaults
|
||||||
|
/// to "pure white".
|
||||||
|
///
|
||||||
|
/// Field uniforms with the same index will be combined into a single binding:
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_render::{render_resource::AsBindGroup};
|
||||||
|
/// # use bevy_color::LinearRgba;
|
||||||
|
/// #[derive(AsBindGroup)]
|
||||||
|
/// struct CoolMaterial {
|
||||||
|
/// #[uniform(0)]
|
||||||
|
/// color: LinearRgba,
|
||||||
|
/// #[uniform(0)]
|
||||||
|
/// roughness: f32,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// In WGSL shaders, the binding would look like this:
|
||||||
|
/// ```wgsl
|
||||||
|
/// struct CoolMaterial {
|
||||||
|
/// color: vec4<f32>,
|
||||||
|
/// roughness: f32,
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> material: CoolMaterial;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Some less common scenarios will require "struct-level" attributes. These are the currently supported struct-level attributes:
|
||||||
|
/// ## `uniform(BINDING_INDEX, ConvertedShaderType)`
|
||||||
|
///
|
||||||
|
/// * This also creates a [`Buffer`] using [`ShaderType`] and binds it as a
|
||||||
|
/// uniform, much like the field-level `uniform` attribute. The difference is
|
||||||
|
/// that the entire [`AsBindGroup`] value is converted to `ConvertedShaderType`,
|
||||||
|
/// which must implement [`ShaderType`], instead of a specific field
|
||||||
|
/// implementing [`ShaderType`]. This is useful if more complicated conversion
|
||||||
|
/// logic is required, or when using bindless mode (see below). The conversion
|
||||||
|
/// is done using the [`AsBindGroupShaderType<ConvertedShaderType>`] trait,
|
||||||
|
/// which is automatically implemented if `&Self` implements
|
||||||
|
/// [`Into<ConvertedShaderType>`]. Outside of bindless mode, only use
|
||||||
|
/// [`AsBindGroupShaderType`] if access to resources like
|
||||||
|
/// [`RenderAssets<GpuImage>`] is required.
|
||||||
|
///
|
||||||
|
/// * In bindless mode (see `bindless(COUNT)`), this attribute becomes
|
||||||
|
/// `uniform(BINDLESS_INDEX, ConvertedShaderType,
|
||||||
|
/// binding_array(BINDING_INDEX))`. The resulting uniform buffers will be
|
||||||
|
/// available in the shader as a binding array at the given `BINDING_INDEX`. The
|
||||||
|
/// `BINDLESS_INDEX` specifies the offset of the buffer in the bindless index
|
||||||
|
/// table.
|
||||||
|
///
|
||||||
|
/// For example, suppose that the material slot is stored in a variable named
|
||||||
|
/// `slot`, the bindless index table is named `material_indices`, and that the
|
||||||
|
/// first field (index 0) of the bindless index table type is named
|
||||||
|
/// `material`. Then specifying `#[uniform(0, StandardMaterialUniform,
|
||||||
|
/// binding_array(10)]` will create a binding array buffer declared in the
|
||||||
|
/// shader as `var<storage> material_array:
|
||||||
|
/// binding_array<StandardMaterialUniform>` and accessible as
|
||||||
|
/// `material_array[material_indices[slot].material]`.
|
||||||
|
///
|
||||||
|
/// ## `data(BINDING_INDEX, ConvertedShaderType, binding_array(BINDING_INDEX))`
|
||||||
|
///
|
||||||
|
/// * This is very similar to `uniform(BINDING_INDEX, ConvertedShaderType,
|
||||||
|
/// binding_array(BINDING_INDEX)` and in fact is identical if bindless mode
|
||||||
|
/// isn't being used. The difference is that, in bindless mode, the `data`
|
||||||
|
/// attribute produces a single buffer containing an array, not an array of
|
||||||
|
/// buffers. For example, suppose you had the following declaration:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[uniform(0, StandardMaterialUniform, binding_array(10))]
|
||||||
|
/// struct StandardMaterial { ... }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// In bindless mode, this will produce a binding matching the following WGSL
|
||||||
|
/// declaration:
|
||||||
|
///
|
||||||
|
/// ```wgsl
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(10) var<storage> material_array: binding_array<StandardMaterial>;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// On the other hand, if you write this declaration:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[data(0, StandardMaterialUniform, binding_array(10))]
|
||||||
|
/// struct StandardMaterial { ... }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Then Bevy produces a binding that matches this WGSL declaration instead:
|
||||||
|
///
|
||||||
|
/// ```wgsl
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(10) var<storage> material_array: array<StandardMaterial>;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// * Just as with the structure-level `uniform` attribute, Bevy converts the
|
||||||
|
/// entire [`AsBindGroup`] to `ConvertedShaderType`, using the
|
||||||
|
/// [`AsBindGroupShaderType<ConvertedShaderType>`] trait.
|
||||||
|
///
|
||||||
|
/// * In non-bindless mode, the structure-level `data` attribute is the same as
|
||||||
|
/// the structure-level `uniform` attribute and produces a single uniform buffer
|
||||||
|
/// in the shader. The above example would result in a binding that looks like
|
||||||
|
/// this in WGSL in non-bindless mode:
|
||||||
|
///
|
||||||
|
/// ```wgsl
|
||||||
|
/// @group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> material: StandardMaterial;
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// * For efficiency reasons, `data` is generally preferred over `uniform`
|
||||||
|
/// unless you need to place your data in individual buffers.
|
||||||
|
///
|
||||||
|
/// ## `bind_group_data(DataType)`
|
||||||
|
///
|
||||||
|
/// * The [`AsBindGroup`] type will be converted to some `DataType` using [`Into<DataType>`] and stored
|
||||||
|
/// as [`AsBindGroup::Data`] as part of the [`AsBindGroup::as_bind_group`] call. This is useful if data needs to be stored alongside
|
||||||
|
/// the generated bind group, such as a unique identifier for a material's bind group. The most common use case for this attribute
|
||||||
|
/// is "shader pipeline specialization". See [`SpecializedRenderPipeline`](crate::render_resource::SpecializedRenderPipeline).
|
||||||
|
///
|
||||||
|
/// ## `bindless`
|
||||||
|
///
|
||||||
|
/// * This switch enables *bindless resources*, which changes the way Bevy
|
||||||
|
/// supplies resources (textures, and samplers) to the shader. When bindless
|
||||||
|
/// resources are enabled, and the current platform supports them, Bevy will
|
||||||
|
/// allocate textures, and samplers into *binding arrays*, separated based on
|
||||||
|
/// type and will supply your shader with indices into those arrays.
|
||||||
|
/// * Bindless textures and samplers are placed into the appropriate global
|
||||||
|
/// array defined in `bevy_render::bindless` (`bindless.wgsl`).
|
||||||
|
/// * Bevy doesn't currently support bindless buffers, except for those created
|
||||||
|
/// with the `uniform(BINDLESS_INDEX, ConvertedShaderType,
|
||||||
|
/// binding_array(BINDING_INDEX))` attribute. If you need to include a buffer in
|
||||||
|
/// your object, and you can't create the data in that buffer with the `uniform`
|
||||||
|
/// attribute, consider a non-bindless object instead.
|
||||||
|
/// * If bindless mode is enabled, the `BINDLESS` definition will be
|
||||||
|
/// available. Because not all platforms support bindless resources, you
|
||||||
|
/// should check for the presence of this definition via `#ifdef` and fall
|
||||||
|
/// back to standard bindings if it isn't present.
|
||||||
|
/// * By default, in bindless mode, binding 0 becomes the *bindless index
|
||||||
|
/// table*, which is an array of structures, each of which contains as many
|
||||||
|
/// fields of type `u32` as the highest binding number in the structure
|
||||||
|
/// annotated with `#[derive(AsBindGroup)]`. Again by default, the *i*th field
|
||||||
|
/// of the bindless index table contains the index of the resource with binding
|
||||||
|
/// *i* within the appropriate binding array.
|
||||||
|
/// * In the case of materials, the index of the applicable table within the
|
||||||
|
/// bindless index table list corresponding to the mesh currently being drawn
|
||||||
|
/// can be retrieved with
|
||||||
|
/// `mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu`.
|
||||||
|
/// * You can limit the size of the bindless slabs to N resources with the
|
||||||
|
/// `limit(N)` declaration. For example, `#[bindless(limit(16))]` ensures that
|
||||||
|
/// each slab will have no more than 16 total resources in it. If you don't
|
||||||
|
/// specify a limit, Bevy automatically picks a reasonable one for the current
|
||||||
|
/// platform.
|
||||||
|
/// * The `index_table(range(M..N), binding(B))` declaration allows you to
|
||||||
|
/// customize the layout of the bindless index table. This is useful for
|
||||||
|
/// materials that are composed of multiple bind groups, such as
|
||||||
|
/// `ExtendedMaterial`. In such cases, there will be multiple bindless index
|
||||||
|
/// tables, so they can't both be assigned to binding 0 or their bindings will
|
||||||
|
/// conflict.
|
||||||
|
/// - The `binding(B)` attribute of the `index_table` attribute allows you to
|
||||||
|
/// customize the binding (`@binding(B)`, in the shader) at which the index
|
||||||
|
/// table will be bound.
|
||||||
|
/// - The `range(M, N)` attribute of the `index_table` attribute allows you to
|
||||||
|
/// change the mapping from the field index in the bindless index table to the
|
||||||
|
/// bindless index. Instead of the field at index $i$ being mapped to the
|
||||||
|
/// bindless index $i$, with the `range(M, N)` attribute the field at index
|
||||||
|
/// $i$ in the bindless index table is mapped to the bindless index $i$ + M.
|
||||||
|
/// The size of the index table will be set to N - M. Note that this may
|
||||||
|
/// result in the table being too small to contain all the bindless bindings.
|
||||||
|
/// * The purpose of bindless mode is to improve performance by reducing
|
||||||
|
/// state changes. By grouping resources together into binding arrays, Bevy
|
||||||
|
/// doesn't have to modify GPU state as often, decreasing API and driver
|
||||||
|
/// overhead.
|
||||||
|
/// * See the `shaders/shader_material_bindless` example for an example of how
|
||||||
|
/// to use bindless mode. See the `shaders/extended_material_bindless` example
|
||||||
|
/// for a more exotic example of bindless mode that demonstrates the
|
||||||
|
/// `index_table` attribute.
|
||||||
|
/// * The following diagram illustrates how bindless mode works using a subset
|
||||||
|
/// of `StandardMaterial`:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// Shader Bindings Sampler Binding Array
|
||||||
|
/// +----+-----------------------------+ +-----------+-----------+-----+
|
||||||
|
/// +---| 0 | material_indices | +->| sampler 0 | sampler 1 | ... |
|
||||||
|
/// | +----+-----------------------------+ | +-----------+-----------+-----+
|
||||||
|
/// | | 1 | bindless_samplers_filtering +--+ ^
|
||||||
|
/// | +----+-----------------------------+ +-------------------------------+
|
||||||
|
/// | | .. | ... | |
|
||||||
|
/// | +----+-----------------------------+ Texture Binding Array |
|
||||||
|
/// | | 5 | bindless_textures_2d +--+ +-----------+-----------+-----+ |
|
||||||
|
/// | +----+-----------------------------+ +->| texture 0 | texture 1 | ... | |
|
||||||
|
/// | | .. | ... | +-----------+-----------+-----+ |
|
||||||
|
/// | +----+-----------------------------+ ^ |
|
||||||
|
/// | + 10 | material_array +--+ +---------------------------+ |
|
||||||
|
/// | +----+-----------------------------+ | | |
|
||||||
|
/// | | Buffer Binding Array | |
|
||||||
|
/// | | +----------+----------+-----+ | |
|
||||||
|
/// | +->| buffer 0 | buffer 1 | ... | | |
|
||||||
|
/// | Material Bindless Indices +----------+----------+-----+ | |
|
||||||
|
/// | +----+-----------------------------+ ^ | |
|
||||||
|
/// +-->| 0 | material +----------+ | |
|
||||||
|
/// +----+-----------------------------+ | |
|
||||||
|
/// | 1 | base_color_texture +---------------------------------------+ |
|
||||||
|
/// +----+-----------------------------+ |
|
||||||
|
/// | 2 | base_color_sampler +-------------------------------------------+
|
||||||
|
/// +----+-----------------------------+
|
||||||
|
/// | .. | ... |
|
||||||
|
/// +----+-----------------------------+
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The previous `CoolMaterial` example illustrating "combining multiple field-level uniform attributes with the same binding index" can
|
||||||
|
/// also be equivalently represented with a single struct-level uniform attribute:
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_render::{render_resource::{AsBindGroup, ShaderType}};
|
||||||
|
/// # use bevy_color::LinearRgba;
|
||||||
|
/// #[derive(AsBindGroup)]
|
||||||
|
/// #[uniform(0, CoolMaterialUniform)]
|
||||||
|
/// struct CoolMaterial {
|
||||||
|
/// color: LinearRgba,
|
||||||
|
/// roughness: f32,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(ShaderType)]
|
||||||
|
/// struct CoolMaterialUniform {
|
||||||
|
/// color: LinearRgba,
|
||||||
|
/// roughness: f32,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl From<&CoolMaterial> for CoolMaterialUniform {
|
||||||
|
/// fn from(material: &CoolMaterial) -> CoolMaterialUniform {
|
||||||
|
/// CoolMaterialUniform {
|
||||||
|
/// color: material.color,
|
||||||
|
/// roughness: material.roughness,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Setting `bind_group_data` looks like this:
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_render::{render_resource::AsBindGroup};
|
||||||
|
/// # use bevy_color::LinearRgba;
|
||||||
|
/// #[derive(AsBindGroup)]
|
||||||
|
/// #[bind_group_data(CoolMaterialKey)]
|
||||||
|
/// struct CoolMaterial {
|
||||||
|
/// #[uniform(0)]
|
||||||
|
/// color: LinearRgba,
|
||||||
|
/// is_shaded: bool,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Materials keys are intended to be small, cheap to hash, and
|
||||||
|
/// // uniquely identify a specific material permutation.
|
||||||
|
/// #[repr(C)]
|
||||||
|
/// #[derive(Copy, Clone, Hash, Eq, PartialEq)]
|
||||||
|
/// struct CoolMaterialKey {
|
||||||
|
/// is_shaded: bool,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl From<&CoolMaterial> for CoolMaterialKey {
|
||||||
|
/// fn from(material: &CoolMaterial) -> CoolMaterialKey {
|
||||||
|
/// CoolMaterialKey {
|
||||||
|
/// is_shaded: material.is_shaded,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub trait AsBindGroup {
|
||||||
|
/// Data that will be stored alongside the "prepared" bind group.
|
||||||
|
type Data: Send + Sync;
|
||||||
|
|
||||||
|
type Param: SystemParam + 'static;
|
||||||
|
|
||||||
|
/// The number of slots per bind group, if bindless mode is enabled.
|
||||||
|
///
|
||||||
|
/// If this bind group doesn't use bindless, then this will be `None`.
|
||||||
|
///
|
||||||
|
/// Note that the *actual* slot count may be different from this value, due
|
||||||
|
/// to platform limitations. For example, if bindless resources aren't
|
||||||
|
/// supported on this platform, the actual slot count will be 1.
|
||||||
|
fn bindless_slot_count() -> Option<BindlessSlabResourceLimit> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if the hardware *actually* supports bindless textures for this
|
||||||
|
/// type, taking the device and driver capabilities into account.
|
||||||
|
///
|
||||||
|
/// If this type doesn't use bindless textures, then the return value from
|
||||||
|
/// this function is meaningless.
|
||||||
|
fn bindless_supported(_: &RenderDevice) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// label
|
||||||
|
fn label() -> &'static str;
|
||||||
|
|
||||||
|
/// Creates a bind group for `self` matching the layout defined in [`AsBindGroup::bind_group_layout`].
|
||||||
|
fn as_bind_group(
|
||||||
|
&self,
|
||||||
|
layout_descriptor: &BindGroupLayoutDescriptor,
|
||||||
|
render_device: &RenderDevice,
|
||||||
|
pipeline_cache: &PipelineCache,
|
||||||
|
param: &mut SystemParamItem<'_, '_, Self::Param>,
|
||||||
|
) -> Result<PreparedBindGroup, AsBindGroupError> {
|
||||||
|
let layout = &pipeline_cache.get_bind_group_layout(layout_descriptor);
|
||||||
|
|
||||||
|
let UnpreparedBindGroup { bindings } =
|
||||||
|
Self::unprepared_bind_group(self, layout, render_device, param, false)?;
|
||||||
|
|
||||||
|
let entries = bindings
|
||||||
|
.iter()
|
||||||
|
.map(|(index, binding)| BindGroupEntry {
|
||||||
|
binding: *index,
|
||||||
|
resource: binding.get_binding(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let bind_group = render_device.create_bind_group(Self::label(), layout, &entries);
|
||||||
|
|
||||||
|
Ok(PreparedBindGroup {
|
||||||
|
bindings,
|
||||||
|
bind_group,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_group_data(&self) -> Self::Data;
|
||||||
|
|
||||||
|
/// Returns a vec of (binding index, `OwnedBindingResource`).
|
||||||
|
///
|
||||||
|
/// In cases where `OwnedBindingResource` is not available (as for bindless
|
||||||
|
/// texture arrays currently), an implementor may return
|
||||||
|
/// `AsBindGroupError::CreateBindGroupDirectly` from this function and
|
||||||
|
/// instead define `as_bind_group` directly. This may prevent certain
|
||||||
|
/// features, such as bindless mode, from working correctly.
|
||||||
|
///
|
||||||
|
/// Set `force_no_bindless` to true to require that bindless textures *not*
|
||||||
|
/// be used. `ExtendedMaterial` uses this in order to ensure that the base
|
||||||
|
/// material doesn't use bindless mode if the extension doesn't.
|
||||||
|
fn unprepared_bind_group(
|
||||||
|
&self,
|
||||||
|
layout: &BindGroupLayout,
|
||||||
|
render_device: &RenderDevice,
|
||||||
|
param: &mut SystemParamItem<'_, '_, Self::Param>,
|
||||||
|
force_no_bindless: bool,
|
||||||
|
) -> Result<UnpreparedBindGroup, AsBindGroupError>;
|
||||||
|
|
||||||
|
/// Creates the bind group layout matching all bind groups returned by
|
||||||
|
/// [`AsBindGroup::as_bind_group`]
|
||||||
|
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
render_device.create_bind_group_layout(
|
||||||
|
Self::label(),
|
||||||
|
&Self::bind_group_layout_entries(render_device, false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the bind group layout descriptor matching all bind groups returned by
|
||||||
|
/// [`AsBindGroup::as_bind_group`]
|
||||||
|
/// TODO: we only need `RenderDevice` to determine if bindless is supported
|
||||||
|
fn bind_group_layout_descriptor(render_device: &RenderDevice) -> BindGroupLayoutDescriptor
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
BindGroupLayoutDescriptor {
|
||||||
|
label: Self::label().into(),
|
||||||
|
entries: Self::bind_group_layout_entries(render_device, false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a vec of bind group layout entries.
|
||||||
|
///
|
||||||
|
/// Set `force_no_bindless` to true to require that bindless textures *not*
|
||||||
|
/// be used. `ExtendedMaterial` uses this in order to ensure that the base
|
||||||
|
/// material doesn't use bindless mode if the extension doesn't.
|
||||||
|
fn bind_group_layout_entries(
|
||||||
|
render_device: &RenderDevice,
|
||||||
|
force_no_bindless: bool,
|
||||||
|
) -> Vec<BindGroupLayoutEntry>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
fn bindless_descriptor() -> Option<BindlessDescriptor> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurs during [`AsBindGroup::as_bind_group`] calls.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AsBindGroupError {
|
||||||
|
/// The bind group could not be generated. Try again next frame.
|
||||||
|
#[error("The bind group could not be generated")]
|
||||||
|
RetryNextUpdate,
|
||||||
|
#[error("Create the bind group via `as_bind_group()` instead")]
|
||||||
|
CreateBindGroupDirectly,
|
||||||
|
#[error("At binding index {0}, the provided image sampler `{1}` does not match the required sampler type(s) `{2}`.")]
|
||||||
|
InvalidSamplerType(u32, String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`].
|
||||||
|
pub struct PreparedBindGroup {
|
||||||
|
pub bindings: BindingResources,
|
||||||
|
pub bind_group: BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a map containing `OwnedBindingResource`s, keyed by the target binding index
|
||||||
|
pub struct UnpreparedBindGroup {
|
||||||
|
pub bindings: BindingResources,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pair of binding index and binding resource, used as part of
|
||||||
|
/// [`PreparedBindGroup`] and [`UnpreparedBindGroup`].
|
||||||
|
#[derive(Deref, DerefMut)]
|
||||||
|
pub struct BindingResources(pub Vec<(u32, OwnedBindingResource)>);
|
||||||
|
|
||||||
|
/// An owned binding resource of any type (ex: a [`Buffer`], [`TextureView`], etc).
|
||||||
|
/// This is used by types like [`PreparedBindGroup`] to hold a single list of all
|
||||||
|
/// render resources used by bindings.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OwnedBindingResource {
|
||||||
|
Buffer(Buffer),
|
||||||
|
TextureView(TextureViewDimension, TextureView),
|
||||||
|
Sampler(SamplerBindingType, Sampler),
|
||||||
|
Data(OwnedData),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data that will be copied into a GPU buffer.
|
||||||
|
///
|
||||||
|
/// This corresponds to the `#[data]` attribute in `AsBindGroup`.
|
||||||
|
#[derive(Debug, Deref, DerefMut)]
|
||||||
|
pub struct OwnedData(pub Vec<u8>);
|
||||||
|
|
||||||
|
impl OwnedBindingResource {
|
||||||
|
/// Creates a [`BindingResource`] reference to this
|
||||||
|
/// [`OwnedBindingResource`].
|
||||||
|
///
|
||||||
|
/// Note that this operation panics if passed a
|
||||||
|
/// [`OwnedBindingResource::Data`], because [`OwnedData`] doesn't itself
|
||||||
|
/// correspond to any binding and instead requires the
|
||||||
|
/// `MaterialBindGroupAllocator` to pack it into a buffer.
|
||||||
|
pub fn get_binding(&self) -> BindingResource<'_> {
|
||||||
|
match self {
|
||||||
|
OwnedBindingResource::Buffer(buffer) => buffer.as_entire_binding(),
|
||||||
|
OwnedBindingResource::TextureView(_, view) => BindingResource::TextureView(view),
|
||||||
|
OwnedBindingResource::Sampler(_, sampler) => BindingResource::Sampler(sampler),
|
||||||
|
OwnedBindingResource::Data(_) => panic!("`OwnedData` has no binding resource"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a value to a [`ShaderType`] for use in a bind group.
|
||||||
|
///
|
||||||
|
/// This is automatically implemented for references that implement [`Into`].
|
||||||
|
/// Generally normal [`Into`] / [`From`] impls should be preferred, but
|
||||||
|
/// sometimes additional runtime metadata is required.
|
||||||
|
/// This exists largely to make some [`AsBindGroup`] use cases easier.
|
||||||
|
pub trait AsBindGroupShaderType<T: ShaderType> {
|
||||||
|
/// Return the `T` [`ShaderType`] for `self`. When used in [`AsBindGroup`]
|
||||||
|
/// derives, it is safe to assume that all images in `self` exist.
|
||||||
|
fn as_bind_group_shader_type(&self, images: &RenderAssets<GpuImage>) -> T;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U: ShaderType> AsBindGroupShaderType<U> for T
|
||||||
|
where
|
||||||
|
for<'a> &'a T: Into<U>,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn as_bind_group_shader_type(&self, _images: &RenderAssets<GpuImage>) -> U {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use bevy_asset::Handle;
|
||||||
|
use bevy_image::Image;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn texture_visibility() {
|
||||||
|
#[expect(
|
||||||
|
dead_code,
|
||||||
|
reason = "This is a derive macro compilation test. It will not be constructed."
|
||||||
|
)]
|
||||||
|
#[derive(AsBindGroup)]
|
||||||
|
pub struct TextureVisibilityTest {
|
||||||
|
#[texture(0, visibility(all))]
|
||||||
|
pub all: Handle<Image>,
|
||||||
|
#[texture(1, visibility(none))]
|
||||||
|
pub none: Handle<Image>,
|
||||||
|
#[texture(2, visibility(fragment))]
|
||||||
|
pub fragment: Handle<Image>,
|
||||||
|
#[texture(3, visibility(vertex))]
|
||||||
|
pub vertex: Handle<Image>,
|
||||||
|
#[texture(4, visibility(compute))]
|
||||||
|
pub compute: Handle<Image>,
|
||||||
|
#[texture(5, visibility(vertex, fragment))]
|
||||||
|
pub vertex_fragment: Handle<Image>,
|
||||||
|
#[texture(6, visibility(vertex, compute))]
|
||||||
|
pub vertex_compute: Handle<Image>,
|
||||||
|
#[texture(7, visibility(fragment, compute))]
|
||||||
|
pub fragment_compute: Handle<Image>,
|
||||||
|
#[texture(8, visibility(vertex, fragment, compute))]
|
||||||
|
pub vertex_fragment_compute: Handle<Image>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
322
third_party/bevy_render/src/render_resource/bind_group_entries.rs
vendored
Normal file
322
third_party/bevy_render/src/render_resource/bind_group_entries.rs
vendored
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
use variadics_please::all_tuples_with_size;
|
||||||
|
use wgpu::{BindGroupEntry, BindingResource};
|
||||||
|
|
||||||
|
use super::{Sampler, TextureView};
|
||||||
|
|
||||||
|
/// Helper for constructing bindgroups.
|
||||||
|
///
|
||||||
|
/// Allows constructing the descriptor's entries as:
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// render_device.create_bind_group(
|
||||||
|
/// "my_bind_group",
|
||||||
|
/// &my_layout,
|
||||||
|
/// &BindGroupEntries::with_indices((
|
||||||
|
/// (2, &my_sampler),
|
||||||
|
/// (3, my_uniform),
|
||||||
|
/// )),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// instead of
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// render_device.create_bind_group(
|
||||||
|
/// "my_bind_group",
|
||||||
|
/// &my_layout,
|
||||||
|
/// &[
|
||||||
|
/// BindGroupEntry {
|
||||||
|
/// binding: 2,
|
||||||
|
/// resource: BindingResource::Sampler(&my_sampler),
|
||||||
|
/// },
|
||||||
|
/// BindGroupEntry {
|
||||||
|
/// binding: 3,
|
||||||
|
/// resource: my_uniform,
|
||||||
|
/// },
|
||||||
|
/// ],
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// or
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// render_device.create_bind_group(
|
||||||
|
/// "my_bind_group",
|
||||||
|
/// &my_layout,
|
||||||
|
/// &BindGroupEntries::sequential((
|
||||||
|
/// &my_sampler,
|
||||||
|
/// my_uniform,
|
||||||
|
/// )),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// instead of
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// render_device.create_bind_group(
|
||||||
|
/// "my_bind_group",
|
||||||
|
/// &my_layout,
|
||||||
|
/// &[
|
||||||
|
/// BindGroupEntry {
|
||||||
|
/// binding: 0,
|
||||||
|
/// resource: BindingResource::Sampler(&my_sampler),
|
||||||
|
/// },
|
||||||
|
/// BindGroupEntry {
|
||||||
|
/// binding: 1,
|
||||||
|
/// resource: my_uniform,
|
||||||
|
/// },
|
||||||
|
/// ],
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// or
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// render_device.create_bind_group(
|
||||||
|
/// "my_bind_group",
|
||||||
|
/// &my_layout,
|
||||||
|
/// &BindGroupEntries::single(my_uniform),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// instead of
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// render_device.create_bind_group(
|
||||||
|
/// "my_bind_group",
|
||||||
|
/// &my_layout,
|
||||||
|
/// &[
|
||||||
|
/// BindGroupEntry {
|
||||||
|
/// binding: 0,
|
||||||
|
/// resource: my_uniform,
|
||||||
|
/// },
|
||||||
|
/// ],
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub struct BindGroupEntries<'b, const N: usize = 1> {
|
||||||
|
entries: [BindGroupEntry<'b>; N],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'b, const N: usize> BindGroupEntries<'b, N> {
|
||||||
|
#[inline]
|
||||||
|
pub fn sequential(resources: impl IntoBindingArray<'b, N>) -> Self {
|
||||||
|
let mut i = 0;
|
||||||
|
Self {
|
||||||
|
entries: resources.into_array().map(|resource| {
|
||||||
|
let binding = i;
|
||||||
|
i += 1;
|
||||||
|
BindGroupEntry { binding, resource }
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn with_indices(indexed_resources: impl IntoIndexedBindingArray<'b, N>) -> Self {
|
||||||
|
Self {
|
||||||
|
entries: indexed_resources
|
||||||
|
.into_array()
|
||||||
|
.map(|(binding, resource)| BindGroupEntry { binding, resource }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'b> BindGroupEntries<'b, 1> {
|
||||||
|
pub fn single(resource: impl IntoBinding<'b>) -> [BindGroupEntry<'b>; 1] {
|
||||||
|
[BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: resource.into_binding(),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'b, const N: usize> core::ops::Deref for BindGroupEntries<'b, N> {
|
||||||
|
type Target = [BindGroupEntry<'b>];
|
||||||
|
|
||||||
|
fn deref(&self) -> &[BindGroupEntry<'b>] {
|
||||||
|
&self.entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntoBinding<'a> {
|
||||||
|
fn into_binding(self) -> BindingResource<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoBinding<'a> for &'a TextureView {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
BindingResource::TextureView(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoBinding<'a> for &'a wgpu::TextureView {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
BindingResource::TextureView(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoBinding<'a> for &'a [&'a wgpu::TextureView] {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
BindingResource::TextureViewArray(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoBinding<'a> for &'a Sampler {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
BindingResource::Sampler(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoBinding<'a> for &'a [&'a wgpu::Sampler] {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
BindingResource::SamplerArray(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoBinding<'a> for BindingResource<'a> {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
BindingResource::Buffer(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoBinding<'a> for &'a [wgpu::BufferBinding<'a>] {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
BindingResource::BufferArray(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntoBindingArray<'b, const N: usize> {
|
||||||
|
fn into_array(self) -> [BindingResource<'b>; N];
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_to_binding_slice {
|
||||||
|
($N: expr, $(#[$meta:meta])* $(($T: ident, $I: ident)),*) => {
|
||||||
|
$(#[$meta])*
|
||||||
|
impl<'b, $($T: IntoBinding<'b>),*> IntoBindingArray<'b, $N> for ($($T,)*) {
|
||||||
|
#[inline]
|
||||||
|
fn into_array(self) -> [BindingResource<'b>; $N] {
|
||||||
|
let ($($I,)*) = self;
|
||||||
|
[$($I.into_binding(), )*]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
all_tuples_with_size!(
|
||||||
|
#[doc(fake_variadic)]
|
||||||
|
impl_to_binding_slice,
|
||||||
|
1,
|
||||||
|
32,
|
||||||
|
T,
|
||||||
|
s
|
||||||
|
);
|
||||||
|
|
||||||
|
pub trait IntoIndexedBindingArray<'b, const N: usize> {
|
||||||
|
fn into_array(self) -> [(u32, BindingResource<'b>); N];
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_to_indexed_binding_slice {
|
||||||
|
($N: expr, $(($T: ident, $S: ident, $I: ident)),*) => {
|
||||||
|
impl<'b, $($T: IntoBinding<'b>),*> IntoIndexedBindingArray<'b, $N> for ($((u32, $T),)*) {
|
||||||
|
#[inline]
|
||||||
|
fn into_array(self) -> [(u32, BindingResource<'b>); $N] {
|
||||||
|
let ($(($S, $I),)*) = self;
|
||||||
|
[$(($S, $I.into_binding())), *]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
all_tuples_with_size!(impl_to_indexed_binding_slice, 1, 32, T, n, s);
|
||||||
|
|
||||||
|
pub struct DynamicBindGroupEntries<'b> {
|
||||||
|
entries: Vec<BindGroupEntry<'b>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'b> Default for DynamicBindGroupEntries<'b> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'b> DynamicBindGroupEntries<'b> {
|
||||||
|
pub fn sequential<const N: usize>(entries: impl IntoBindingArray<'b, N>) -> Self {
|
||||||
|
Self {
|
||||||
|
entries: entries
|
||||||
|
.into_array()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, resource)| BindGroupEntry {
|
||||||
|
binding: ix as u32,
|
||||||
|
resource,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend_sequential<const N: usize>(
|
||||||
|
mut self,
|
||||||
|
entries: impl IntoBindingArray<'b, N>,
|
||||||
|
) -> Self {
|
||||||
|
let start = self.entries.last().unwrap().binding + 1;
|
||||||
|
self.entries.extend(
|
||||||
|
entries
|
||||||
|
.into_array()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, resource)| BindGroupEntry {
|
||||||
|
binding: start + ix as u32,
|
||||||
|
resource,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_indices<const N: usize>(entries: impl IntoIndexedBindingArray<'b, N>) -> Self {
|
||||||
|
Self {
|
||||||
|
entries: entries
|
||||||
|
.into_array()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(binding, resource)| BindGroupEntry { binding, resource })
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
entries: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend_with_indices<const N: usize>(
|
||||||
|
mut self,
|
||||||
|
entries: impl IntoIndexedBindingArray<'b, N>,
|
||||||
|
) -> Self {
|
||||||
|
self.entries.extend(
|
||||||
|
entries
|
||||||
|
.into_array()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(binding, resource)| BindGroupEntry { binding, resource }),
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'b> core::ops::Deref for DynamicBindGroupEntries<'b> {
|
||||||
|
type Target = [BindGroupEntry<'b>];
|
||||||
|
|
||||||
|
fn deref(&self) -> &[BindGroupEntry<'b>] {
|
||||||
|
&self.entries
|
||||||
|
}
|
||||||
|
}
|
||||||
63
third_party/bevy_render/src/render_resource/bind_group_layout.rs
vendored
Normal file
63
third_party/bevy_render/src/render_resource/bind_group_layout.rs
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use crate::{define_atomic_id, renderer::WgpuWrapper};
|
||||||
|
use core::ops::Deref;
|
||||||
|
|
||||||
|
define_atomic_id!(BindGroupLayoutId);
|
||||||
|
|
||||||
|
/// Bind group layouts define the interface of resources (e.g. buffers, textures, samplers)
|
||||||
|
/// for a shader. The actual resource binding is done via a [`BindGroup`](super::BindGroup).
|
||||||
|
///
|
||||||
|
/// This is a lightweight thread-safe wrapper around wgpu's own [`BindGroupLayout`](wgpu::BindGroupLayout),
|
||||||
|
/// which can be cloned as needed to workaround lifetime management issues. It may be converted
|
||||||
|
/// from and dereferences to wgpu's [`BindGroupLayout`](wgpu::BindGroupLayout).
|
||||||
|
///
|
||||||
|
/// Can be created via [`RenderDevice::create_bind_group_layout`](crate::renderer::RenderDevice::create_bind_group_layout).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BindGroupLayout {
|
||||||
|
id: BindGroupLayoutId,
|
||||||
|
value: WgpuWrapper<wgpu::BindGroupLayout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BindGroupLayout {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for BindGroupLayout {}
|
||||||
|
|
||||||
|
impl core::hash::Hash for BindGroupLayout {
|
||||||
|
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.0.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindGroupLayout {
|
||||||
|
/// Returns the [`BindGroupLayoutId`] representing the unique ID of the bind group layout.
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> BindGroupLayoutId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn value(&self) -> &wgpu::BindGroupLayout {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wgpu::BindGroupLayout> for BindGroupLayout {
|
||||||
|
fn from(value: wgpu::BindGroupLayout) -> Self {
|
||||||
|
BindGroupLayout {
|
||||||
|
id: BindGroupLayoutId::new(),
|
||||||
|
value: WgpuWrapper::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for BindGroupLayout {
|
||||||
|
type Target = wgpu::BindGroupLayout;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
592
third_party/bevy_render/src/render_resource/bind_group_layout_entries.rs
vendored
Normal file
592
third_party/bevy_render/src/render_resource/bind_group_layout_entries.rs
vendored
Normal file
@ -0,0 +1,592 @@
|
|||||||
|
use core::num::NonZero;
|
||||||
|
use variadics_please::all_tuples_with_size;
|
||||||
|
use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages};
|
||||||
|
|
||||||
|
/// Helper for constructing bind group layouts.
|
||||||
|
///
|
||||||
|
/// Allows constructing the layout's entries as:
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// let layout = render_device.create_bind_group_layout(
|
||||||
|
/// "my_bind_group_layout",
|
||||||
|
/// &BindGroupLayoutEntries::with_indices(
|
||||||
|
/// // The layout entries will only be visible in the fragment stage
|
||||||
|
/// ShaderStages::FRAGMENT,
|
||||||
|
/// (
|
||||||
|
/// // Screen texture
|
||||||
|
/// (2, texture_2d(TextureSampleType::Float { filterable: true })),
|
||||||
|
/// // Sampler
|
||||||
|
/// (3, sampler(SamplerBindingType::Filtering)),
|
||||||
|
/// ),
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// instead of
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// let layout = render_device.create_bind_group_layout(
|
||||||
|
/// "my_bind_group_layout",
|
||||||
|
/// &[
|
||||||
|
/// // Screen texture
|
||||||
|
/// BindGroupLayoutEntry {
|
||||||
|
/// binding: 2,
|
||||||
|
/// visibility: ShaderStages::FRAGMENT,
|
||||||
|
/// ty: BindingType::Texture {
|
||||||
|
/// sample_type: TextureSampleType::Float { filterable: true },
|
||||||
|
/// view_dimension: TextureViewDimension::D2,
|
||||||
|
/// multisampled: false,
|
||||||
|
/// },
|
||||||
|
/// count: None,
|
||||||
|
/// },
|
||||||
|
/// // Sampler
|
||||||
|
/// BindGroupLayoutEntry {
|
||||||
|
/// binding: 3,
|
||||||
|
/// visibility: ShaderStages::FRAGMENT,
|
||||||
|
/// ty: BindingType::Sampler(SamplerBindingType::Filtering),
|
||||||
|
/// count: None,
|
||||||
|
/// },
|
||||||
|
/// ],
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// or
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// render_device.create_bind_group_layout(
|
||||||
|
/// "my_bind_group_layout",
|
||||||
|
/// &BindGroupLayoutEntries::sequential(
|
||||||
|
/// ShaderStages::FRAGMENT,
|
||||||
|
/// (
|
||||||
|
/// // Screen texture
|
||||||
|
/// texture_2d(TextureSampleType::Float { filterable: true }),
|
||||||
|
/// // Sampler
|
||||||
|
/// sampler(SamplerBindingType::Filtering),
|
||||||
|
/// ),
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// instead of
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// let layout = render_device.create_bind_group_layout(
|
||||||
|
/// "my_bind_group_layout",
|
||||||
|
/// &[
|
||||||
|
/// // Screen texture
|
||||||
|
/// BindGroupLayoutEntry {
|
||||||
|
/// binding: 0,
|
||||||
|
/// visibility: ShaderStages::FRAGMENT,
|
||||||
|
/// ty: BindingType::Texture {
|
||||||
|
/// sample_type: TextureSampleType::Float { filterable: true },
|
||||||
|
/// view_dimension: TextureViewDimension::D2,
|
||||||
|
/// multisampled: false,
|
||||||
|
/// },
|
||||||
|
/// count: None,
|
||||||
|
/// },
|
||||||
|
/// // Sampler
|
||||||
|
/// BindGroupLayoutEntry {
|
||||||
|
/// binding: 1,
|
||||||
|
/// visibility: ShaderStages::FRAGMENT,
|
||||||
|
/// ty: BindingType::Sampler(SamplerBindingType::Filtering),
|
||||||
|
/// count: None,
|
||||||
|
/// },
|
||||||
|
/// ],
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// or
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// render_device.create_bind_group_layout(
|
||||||
|
/// "my_bind_group_layout",
|
||||||
|
/// &BindGroupLayoutEntries::single(
|
||||||
|
/// ShaderStages::FRAGMENT,
|
||||||
|
/// texture_2d(TextureSampleType::Float { filterable: true }),
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// instead of
|
||||||
|
///
|
||||||
|
/// ```ignore (render_device cannot be easily accessed)
|
||||||
|
/// let layout = render_device.create_bind_group_layout(
|
||||||
|
/// "my_bind_group_layout",
|
||||||
|
/// &[
|
||||||
|
/// BindGroupLayoutEntry {
|
||||||
|
/// binding: 0,
|
||||||
|
/// visibility: ShaderStages::FRAGMENT,
|
||||||
|
/// ty: BindingType::Texture {
|
||||||
|
/// sample_type: TextureSampleType::Float { filterable: true },
|
||||||
|
/// view_dimension: TextureViewDimension::D2,
|
||||||
|
/// multisampled: false,
|
||||||
|
/// },
|
||||||
|
/// count: None,
|
||||||
|
/// },
|
||||||
|
/// ],
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct BindGroupLayoutEntryBuilder {
|
||||||
|
ty: BindingType,
|
||||||
|
visibility: Option<ShaderStages>,
|
||||||
|
count: Option<NonZero<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindGroupLayoutEntryBuilder {
|
||||||
|
pub fn visibility(mut self, visibility: ShaderStages) -> Self {
|
||||||
|
self.visibility = Some(visibility);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(mut self, count: NonZero<u32>) -> Self {
|
||||||
|
self.count = Some(count);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&self, binding: u32, default_visibility: ShaderStages) -> BindGroupLayoutEntry {
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding,
|
||||||
|
ty: self.ty,
|
||||||
|
visibility: self.visibility.unwrap_or(default_visibility),
|
||||||
|
count: self.count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BindGroupLayoutEntries<const N: usize> {
|
||||||
|
entries: [BindGroupLayoutEntry; N],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> BindGroupLayoutEntries<N> {
|
||||||
|
#[inline]
|
||||||
|
pub fn sequential(
|
||||||
|
default_visibility: ShaderStages,
|
||||||
|
entries_ext: impl IntoBindGroupLayoutEntryBuilderArray<N>,
|
||||||
|
) -> Self {
|
||||||
|
let mut i = 0;
|
||||||
|
Self {
|
||||||
|
entries: entries_ext.into_array().map(|entry| {
|
||||||
|
let binding = i;
|
||||||
|
i += 1;
|
||||||
|
entry.build(binding, default_visibility)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn with_indices(
|
||||||
|
default_visibility: ShaderStages,
|
||||||
|
indexed_entries: impl IntoIndexedBindGroupLayoutEntryBuilderArray<N>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
entries: indexed_entries
|
||||||
|
.into_array()
|
||||||
|
.map(|(binding, entry)| entry.build(binding, default_visibility)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindGroupLayoutEntries<1> {
|
||||||
|
pub fn single(
|
||||||
|
visibility: ShaderStages,
|
||||||
|
resource: impl IntoBindGroupLayoutEntryBuilder,
|
||||||
|
) -> [BindGroupLayoutEntry; 1] {
|
||||||
|
[resource
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
.build(0, visibility)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> core::ops::Deref for BindGroupLayoutEntries<N> {
|
||||||
|
type Target = [BindGroupLayoutEntry];
|
||||||
|
fn deref(&self) -> &[BindGroupLayoutEntry] {
|
||||||
|
&self.entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntoBindGroupLayoutEntryBuilder {
|
||||||
|
fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoBindGroupLayoutEntryBuilder for BindingType {
|
||||||
|
fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindGroupLayoutEntryBuilder {
|
||||||
|
ty: self,
|
||||||
|
visibility: None,
|
||||||
|
count: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoBindGroupLayoutEntryBuilder for BindGroupLayoutEntry {
|
||||||
|
fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder {
|
||||||
|
if self.binding != u32::MAX {
|
||||||
|
tracing::warn!("The BindGroupLayoutEntries api ignores the binding index when converting a raw wgpu::BindGroupLayoutEntry. You can ignore this warning by setting it to u32::MAX.");
|
||||||
|
}
|
||||||
|
BindGroupLayoutEntryBuilder {
|
||||||
|
ty: self.ty,
|
||||||
|
visibility: Some(self.visibility),
|
||||||
|
count: self.count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoBindGroupLayoutEntryBuilder for BindGroupLayoutEntryBuilder {
|
||||||
|
fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntoBindGroupLayoutEntryBuilderArray<const N: usize> {
|
||||||
|
fn into_array(self) -> [BindGroupLayoutEntryBuilder; N];
|
||||||
|
}
|
||||||
|
macro_rules! impl_to_binding_type_slice {
|
||||||
|
($N: expr, $(#[$meta:meta])* $(($T: ident, $I: ident)),*) => {
|
||||||
|
$(#[$meta])*
|
||||||
|
impl<$($T: IntoBindGroupLayoutEntryBuilder),*> IntoBindGroupLayoutEntryBuilderArray<$N> for ($($T,)*) {
|
||||||
|
#[inline]
|
||||||
|
fn into_array(self) -> [BindGroupLayoutEntryBuilder; $N] {
|
||||||
|
let ($($I,)*) = self;
|
||||||
|
[$($I.into_bind_group_layout_entry_builder(), )*]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all_tuples_with_size!(
|
||||||
|
#[doc(fake_variadic)]
|
||||||
|
impl_to_binding_type_slice,
|
||||||
|
1,
|
||||||
|
32,
|
||||||
|
T,
|
||||||
|
s
|
||||||
|
);
|
||||||
|
|
||||||
|
pub trait IntoIndexedBindGroupLayoutEntryBuilderArray<const N: usize> {
|
||||||
|
fn into_array(self) -> [(u32, BindGroupLayoutEntryBuilder); N];
|
||||||
|
}
|
||||||
|
macro_rules! impl_to_indexed_binding_type_slice {
|
||||||
|
($N: expr, $(($T: ident, $S: ident, $I: ident)),*) => {
|
||||||
|
impl<$($T: IntoBindGroupLayoutEntryBuilder),*> IntoIndexedBindGroupLayoutEntryBuilderArray<$N> for ($((u32, $T),)*) {
|
||||||
|
#[inline]
|
||||||
|
fn into_array(self) -> [(u32, BindGroupLayoutEntryBuilder); $N] {
|
||||||
|
let ($(($S, $I),)*) = self;
|
||||||
|
[$(($S, $I.into_bind_group_layout_entry_builder())), *]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all_tuples_with_size!(impl_to_indexed_binding_type_slice, 1, 32, T, n, s);
|
||||||
|
|
||||||
|
impl<const N: usize> IntoBindGroupLayoutEntryBuilderArray<N> for [BindGroupLayoutEntry; N] {
|
||||||
|
fn into_array(self) -> [BindGroupLayoutEntryBuilder; N] {
|
||||||
|
self.map(IntoBindGroupLayoutEntryBuilder::into_bind_group_layout_entry_builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DynamicBindGroupLayoutEntries {
|
||||||
|
default_visibility: ShaderStages,
|
||||||
|
entries: Vec<BindGroupLayoutEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicBindGroupLayoutEntries {
|
||||||
|
pub fn sequential<const N: usize>(
|
||||||
|
default_visibility: ShaderStages,
|
||||||
|
entries: impl IntoBindGroupLayoutEntryBuilderArray<N>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
default_visibility,
|
||||||
|
entries: entries
|
||||||
|
.into_array()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, resource)| resource.build(ix as u32, default_visibility))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend_sequential<const N: usize>(
|
||||||
|
mut self,
|
||||||
|
entries: impl IntoBindGroupLayoutEntryBuilderArray<N>,
|
||||||
|
) -> Self {
|
||||||
|
let start = self.entries.last().unwrap().binding + 1;
|
||||||
|
self.entries.extend(
|
||||||
|
entries
|
||||||
|
.into_array()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, resource)| resource.build(start + ix as u32, self.default_visibility)),
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_indices<const N: usize>(
|
||||||
|
default_visibility: ShaderStages,
|
||||||
|
entries: impl IntoIndexedBindGroupLayoutEntryBuilderArray<N>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
default_visibility,
|
||||||
|
entries: entries
|
||||||
|
.into_array()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(binding, resource)| resource.build(binding, default_visibility))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(default_visibility: ShaderStages) -> Self {
|
||||||
|
Self {
|
||||||
|
default_visibility,
|
||||||
|
entries: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extend_with_indices<const N: usize>(
|
||||||
|
mut self,
|
||||||
|
entries: impl IntoIndexedBindGroupLayoutEntryBuilderArray<N>,
|
||||||
|
) -> Self {
|
||||||
|
self.entries.extend(
|
||||||
|
entries
|
||||||
|
.into_array()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(binding, resource)| resource.build(binding, self.default_visibility)),
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::ops::Deref for DynamicBindGroupLayoutEntries {
|
||||||
|
type Target = [BindGroupLayoutEntry];
|
||||||
|
|
||||||
|
fn deref(&self) -> &[BindGroupLayoutEntry] {
|
||||||
|
&self.entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod binding_types {
|
||||||
|
use crate::render_resource::{
|
||||||
|
BufferBindingType, SamplerBindingType, TextureSampleType, TextureViewDimension,
|
||||||
|
};
|
||||||
|
use core::num::NonZero;
|
||||||
|
use encase::ShaderType;
|
||||||
|
use wgpu::{StorageTextureAccess, TextureFormat};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn storage_buffer<T: ShaderType>(has_dynamic_offset: bool) -> BindGroupLayoutEntryBuilder {
|
||||||
|
storage_buffer_sized(has_dynamic_offset, Some(T::min_size()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn storage_buffer_sized(
|
||||||
|
has_dynamic_offset: bool,
|
||||||
|
min_binding_size: Option<NonZero<u64>>,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Buffer {
|
||||||
|
ty: BufferBindingType::Storage { read_only: false },
|
||||||
|
has_dynamic_offset,
|
||||||
|
min_binding_size,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn storage_buffer_read_only<T: ShaderType>(
|
||||||
|
has_dynamic_offset: bool,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
storage_buffer_read_only_sized(has_dynamic_offset, Some(T::min_size()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn storage_buffer_read_only_sized(
|
||||||
|
has_dynamic_offset: bool,
|
||||||
|
min_binding_size: Option<NonZero<u64>>,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Buffer {
|
||||||
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
|
has_dynamic_offset,
|
||||||
|
min_binding_size,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uniform_buffer<T: ShaderType>(has_dynamic_offset: bool) -> BindGroupLayoutEntryBuilder {
|
||||||
|
uniform_buffer_sized(has_dynamic_offset, Some(T::min_size()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uniform_buffer_sized(
|
||||||
|
has_dynamic_offset: bool,
|
||||||
|
min_binding_size: Option<NonZero<u64>>,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Buffer {
|
||||||
|
ty: BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset,
|
||||||
|
min_binding_size,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_1d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::D1,
|
||||||
|
multisampled: false,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_2d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::D2,
|
||||||
|
multisampled: false,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_2d_multisampled(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::D2,
|
||||||
|
multisampled: true,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_2d_array(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::D2Array,
|
||||||
|
multisampled: false,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_2d_array_multisampled(
|
||||||
|
sample_type: TextureSampleType,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::D2Array,
|
||||||
|
multisampled: true,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_depth_2d() -> BindGroupLayoutEntryBuilder {
|
||||||
|
texture_2d(TextureSampleType::Depth).into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_depth_2d_multisampled() -> BindGroupLayoutEntryBuilder {
|
||||||
|
texture_2d_multisampled(TextureSampleType::Depth).into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_cube(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::Cube,
|
||||||
|
multisampled: false,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_cube_multisampled(
|
||||||
|
sample_type: TextureSampleType,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::Cube,
|
||||||
|
multisampled: true,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_cube_array(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::CubeArray,
|
||||||
|
multisampled: false,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_cube_array_multisampled(
|
||||||
|
sample_type: TextureSampleType,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::CubeArray,
|
||||||
|
multisampled: true,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_3d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::D3,
|
||||||
|
multisampled: false,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_3d_multisampled(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Texture {
|
||||||
|
sample_type,
|
||||||
|
view_dimension: TextureViewDimension::D3,
|
||||||
|
multisampled: true,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sampler(sampler_binding_type: SamplerBindingType) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::Sampler(sampler_binding_type).into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_storage_2d(
|
||||||
|
format: TextureFormat,
|
||||||
|
access: StorageTextureAccess,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::StorageTexture {
|
||||||
|
access,
|
||||||
|
format,
|
||||||
|
view_dimension: TextureViewDimension::D2,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_storage_2d_array(
|
||||||
|
format: TextureFormat,
|
||||||
|
access: StorageTextureAccess,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::StorageTexture {
|
||||||
|
access,
|
||||||
|
format,
|
||||||
|
view_dimension: TextureViewDimension::D2Array,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_storage_3d(
|
||||||
|
format: TextureFormat,
|
||||||
|
access: StorageTextureAccess,
|
||||||
|
) -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::StorageTexture {
|
||||||
|
access,
|
||||||
|
format,
|
||||||
|
view_dimension: TextureViewDimension::D3,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::AccelerationStructure {
|
||||||
|
vertex_return: false,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn acceleration_structure_vertex_return() -> BindGroupLayoutEntryBuilder {
|
||||||
|
BindingType::AccelerationStructure {
|
||||||
|
vertex_return: true,
|
||||||
|
}
|
||||||
|
.into_bind_group_layout_entry_builder()
|
||||||
|
}
|
||||||
|
}
|
||||||
374
third_party/bevy_render/src/render_resource/bindless.rs
vendored
Normal file
374
third_party/bevy_render/src/render_resource/bindless.rs
vendored
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
//! Types and functions relating to bindless resources.
|
||||||
|
|
||||||
|
use alloc::borrow::Cow;
|
||||||
|
use core::{
|
||||||
|
num::{NonZeroU32, NonZeroU64},
|
||||||
|
ops::Range,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use wgpu::{
|
||||||
|
BindGroupLayoutEntry, SamplerBindingType, ShaderStages, TextureSampleType, TextureViewDimension,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::render_resource::binding_types::storage_buffer_read_only_sized;
|
||||||
|
|
||||||
|
use super::binding_types::{
|
||||||
|
sampler, texture_1d, texture_2d, texture_2d_array, texture_3d, texture_cube, texture_cube_array,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The default value for the number of resources that can be stored in a slab
|
||||||
|
/// on this platform.
|
||||||
|
///
|
||||||
|
/// See the documentation for [`BindlessSlabResourceLimit`] for more
|
||||||
|
/// information.
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
|
pub const AUTO_BINDLESS_SLAB_RESOURCE_LIMIT: u32 = 64;
|
||||||
|
/// The default value for the number of resources that can be stored in a slab
|
||||||
|
/// on this platform.
|
||||||
|
///
|
||||||
|
/// See the documentation for [`BindlessSlabResourceLimit`] for more
|
||||||
|
/// information.
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
|
||||||
|
pub const AUTO_BINDLESS_SLAB_RESOURCE_LIMIT: u32 = 2048;
|
||||||
|
|
||||||
|
/// The binding numbers for the built-in binding arrays of each bindless
|
||||||
|
/// resource type.
|
||||||
|
///
|
||||||
|
/// In the case of materials, the material allocator manages these binding
|
||||||
|
/// arrays.
|
||||||
|
///
|
||||||
|
/// `bindless.wgsl` contains declarations of these arrays for use in your
|
||||||
|
/// shaders. If you change these, make sure to update that file as well.
|
||||||
|
pub static BINDING_NUMBERS: [(BindlessResourceType, BindingNumber); 9] = [
|
||||||
|
(BindlessResourceType::SamplerFiltering, BindingNumber(1)),
|
||||||
|
(BindlessResourceType::SamplerNonFiltering, BindingNumber(2)),
|
||||||
|
(BindlessResourceType::SamplerComparison, BindingNumber(3)),
|
||||||
|
(BindlessResourceType::Texture1d, BindingNumber(4)),
|
||||||
|
(BindlessResourceType::Texture2d, BindingNumber(5)),
|
||||||
|
(BindlessResourceType::Texture2dArray, BindingNumber(6)),
|
||||||
|
(BindlessResourceType::Texture3d, BindingNumber(7)),
|
||||||
|
(BindlessResourceType::TextureCube, BindingNumber(8)),
|
||||||
|
(BindlessResourceType::TextureCubeArray, BindingNumber(9)),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The maximum number of resources that can be stored in a slab.
|
||||||
|
///
|
||||||
|
/// This limit primarily exists in order to work around `wgpu` performance
|
||||||
|
/// problems involving large numbers of bindless resources. Also, some
|
||||||
|
/// platforms, such as Metal, currently enforce limits on the number of
|
||||||
|
/// resources in use.
|
||||||
|
///
|
||||||
|
/// This corresponds to `LIMIT` in the `#[bindless(LIMIT)]` attribute when
|
||||||
|
/// deriving [`crate::render_resource::AsBindGroup`].
|
||||||
|
#[derive(Clone, Copy, Default, PartialEq, Debug)]
|
||||||
|
pub enum BindlessSlabResourceLimit {
|
||||||
|
/// Allows the renderer to choose a reasonable value for the resource limit
|
||||||
|
/// based on the platform.
|
||||||
|
///
|
||||||
|
/// This value has been tuned, so you should default to this value unless
|
||||||
|
/// you have special platform-specific considerations that prevent you from
|
||||||
|
/// using it.
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
|
||||||
|
/// A custom value for the resource limit.
|
||||||
|
///
|
||||||
|
/// Bevy will allocate no more than this number of resources in a slab,
|
||||||
|
/// unless exceeding this value is necessary in order to allocate at all
|
||||||
|
/// (i.e. unless the number of bindless resources in your bind group exceeds
|
||||||
|
/// this value), in which case Bevy can exceed it.
|
||||||
|
Custom(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about the bindless resources in this object.
|
||||||
|
///
|
||||||
|
/// The material bind group allocator uses this descriptor in order to create
|
||||||
|
/// and maintain bind groups. The fields within this bindless descriptor are
|
||||||
|
/// [`Cow`]s in order to support both the common case in which the fields are
|
||||||
|
/// simply `static` constants and the more unusual case in which the fields are
|
||||||
|
/// dynamically generated efficiently. An example of the latter case is
|
||||||
|
/// `ExtendedMaterial`, which needs to assemble a bindless descriptor from those
|
||||||
|
/// of the base material and the material extension at runtime.
|
||||||
|
///
|
||||||
|
/// This structure will only be present if this object is bindless.
|
||||||
|
pub struct BindlessDescriptor {
|
||||||
|
/// The bindless resource types that this object uses, in order of bindless
|
||||||
|
/// index.
|
||||||
|
///
|
||||||
|
/// The resource assigned to binding index 0 will be at index 0, the
|
||||||
|
/// resource assigned to binding index will be at index 1 in this array, and
|
||||||
|
/// so on. Unused binding indices are set to [`BindlessResourceType::None`].
|
||||||
|
pub resources: Cow<'static, [BindlessResourceType]>,
|
||||||
|
/// The [`BindlessBufferDescriptor`] for each bindless buffer that this
|
||||||
|
/// object uses.
|
||||||
|
///
|
||||||
|
/// The order of this array is irrelevant.
|
||||||
|
pub buffers: Cow<'static, [BindlessBufferDescriptor]>,
|
||||||
|
/// The [`BindlessIndexTableDescriptor`]s describing each bindless index
|
||||||
|
/// table.
|
||||||
|
///
|
||||||
|
/// This list must be sorted by the first bindless index.
|
||||||
|
pub index_tables: Cow<'static, [BindlessIndexTableDescriptor]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of potentially-bindless resource.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
|
pub enum BindlessResourceType {
|
||||||
|
/// No bindless resource.
|
||||||
|
///
|
||||||
|
/// This is used as a placeholder to fill holes in the
|
||||||
|
/// [`BindlessDescriptor::resources`] list.
|
||||||
|
None,
|
||||||
|
/// A storage buffer.
|
||||||
|
Buffer,
|
||||||
|
/// A filtering sampler.
|
||||||
|
SamplerFiltering,
|
||||||
|
/// A non-filtering sampler (nearest neighbor).
|
||||||
|
SamplerNonFiltering,
|
||||||
|
/// A comparison sampler (typically used for shadow maps).
|
||||||
|
SamplerComparison,
|
||||||
|
/// A 1D texture.
|
||||||
|
Texture1d,
|
||||||
|
/// A 2D texture.
|
||||||
|
Texture2d,
|
||||||
|
/// A 2D texture array.
|
||||||
|
///
|
||||||
|
/// Note that this differs from a binding array. 2D texture arrays must all
|
||||||
|
/// have the same size and format.
|
||||||
|
Texture2dArray,
|
||||||
|
/// A 3D texture.
|
||||||
|
Texture3d,
|
||||||
|
/// A cubemap texture.
|
||||||
|
TextureCube,
|
||||||
|
/// A cubemap texture array.
|
||||||
|
///
|
||||||
|
/// Note that this differs from a binding array. Cubemap texture arrays must
|
||||||
|
/// all have the same size and format.
|
||||||
|
TextureCubeArray,
|
||||||
|
/// Multiple instances of plain old data concatenated into a single buffer.
|
||||||
|
///
|
||||||
|
/// This corresponds to the `#[data]` declaration in
|
||||||
|
/// [`crate::render_resource::AsBindGroup`].
|
||||||
|
///
|
||||||
|
/// Note that this resource doesn't itself map to a GPU-level binding
|
||||||
|
/// resource and instead depends on the `MaterialBindGroupAllocator` to
|
||||||
|
/// create a binding resource for it.
|
||||||
|
DataBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a bindless buffer.
|
||||||
|
///
|
||||||
|
/// Unlike samplers and textures, each buffer in a bind group gets its own
|
||||||
|
/// unique bind group entry. That is, there isn't any `bindless_buffers` binding
|
||||||
|
/// array to go along with `bindless_textures_2d`,
|
||||||
|
/// `bindless_samplers_filtering`, etc. Therefore, this descriptor contains two
|
||||||
|
/// indices: the *binding number* and the *bindless index*. The binding number
|
||||||
|
/// is the `@binding` number used in the shader, while the bindless index is the
|
||||||
|
/// index of the buffer in the bindless index table (which is itself
|
||||||
|
/// conventionally bound to binding number 0).
|
||||||
|
///
|
||||||
|
/// When declaring the buffer in a derived implementation
|
||||||
|
/// [`crate::render_resource::AsBindGroup`] with syntax like
|
||||||
|
/// `#[uniform(BINDLESS_INDEX, StandardMaterialUniform,
|
||||||
|
/// bindless(BINDING_NUMBER)]`, the bindless index is `BINDLESS_INDEX`, and the
|
||||||
|
/// binding number is `BINDING_NUMBER`. Note the order.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct BindlessBufferDescriptor {
|
||||||
|
/// The actual binding number of the buffer.
|
||||||
|
///
|
||||||
|
/// This is declared with `@binding` in WGSL. When deriving
|
||||||
|
/// [`crate::render_resource::AsBindGroup`], this is the `BINDING_NUMBER` in
|
||||||
|
/// `#[uniform(BINDLESS_INDEX, StandardMaterialUniform,
|
||||||
|
/// bindless(BINDING_NUMBER)]`.
|
||||||
|
pub binding_number: BindingNumber,
|
||||||
|
/// The index of the buffer in the bindless index table.
|
||||||
|
///
|
||||||
|
/// In the shader, this is the index into the table bound to binding 0. When
|
||||||
|
/// deriving [`crate::render_resource::AsBindGroup`], this is the
|
||||||
|
/// `BINDLESS_INDEX` in `#[uniform(BINDLESS_INDEX, StandardMaterialUniform,
|
||||||
|
/// bindless(BINDING_NUMBER)]`.
|
||||||
|
pub bindless_index: BindlessIndex,
|
||||||
|
/// The size of the buffer in bytes, if known.
|
||||||
|
pub size: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the layout of the bindless index table, which maps bindless
|
||||||
|
/// indices to indices within the binding arrays.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BindlessIndexTableDescriptor {
|
||||||
|
/// The range of bindless indices that this descriptor covers.
|
||||||
|
pub indices: Range<BindlessIndex>,
|
||||||
|
/// The binding at which the index table itself will be bound.
|
||||||
|
///
|
||||||
|
/// By default, this is binding 0, but it can be changed with the
|
||||||
|
/// `#[bindless(index_table(binding(B)))]` attribute.
|
||||||
|
pub binding_number: BindingNumber,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The index of the actual binding in the bind group.
|
||||||
|
///
|
||||||
|
/// This is the value specified in WGSL as `@binding`.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Deref, DerefMut)]
|
||||||
|
pub struct BindingNumber(pub u32);
|
||||||
|
|
||||||
|
/// The index in the bindless index table.
|
||||||
|
///
|
||||||
|
/// This table is conventionally bound to binding number 0.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Debug, Deref, DerefMut)]
|
||||||
|
pub struct BindlessIndex(pub u32);
|
||||||
|
|
||||||
|
/// Creates the bind group layout entries common to all shaders that use
|
||||||
|
/// bindless bind groups.
|
||||||
|
///
|
||||||
|
/// `bindless_resource_count` specifies the total number of bindless resources.
|
||||||
|
/// `bindless_slab_resource_limit` specifies the resolved
|
||||||
|
/// [`BindlessSlabResourceLimit`] value.
|
||||||
|
pub fn create_bindless_bind_group_layout_entries(
|
||||||
|
bindless_index_table_length: u32,
|
||||||
|
bindless_slab_resource_limit: u32,
|
||||||
|
bindless_index_table_binding_number: BindingNumber,
|
||||||
|
) -> Vec<BindGroupLayoutEntry> {
|
||||||
|
let bindless_slab_resource_limit =
|
||||||
|
NonZeroU32::new(bindless_slab_resource_limit).expect("Bindless slot count must be nonzero");
|
||||||
|
|
||||||
|
// The maximum size of a binding array is the
|
||||||
|
// `bindless_slab_resource_limit`, which would occur if all of the bindless
|
||||||
|
// resources were of the same type. So we create our binding arrays with
|
||||||
|
// that size.
|
||||||
|
|
||||||
|
vec![
|
||||||
|
// Start with the bindless index table, bound to binding number 0.
|
||||||
|
storage_buffer_read_only_sized(
|
||||||
|
false,
|
||||||
|
NonZeroU64::new(bindless_index_table_length as u64 * size_of::<u32>() as u64),
|
||||||
|
)
|
||||||
|
.build(
|
||||||
|
*bindless_index_table_binding_number,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
// Continue with the common bindless resource arrays.
|
||||||
|
sampler(SamplerBindingType::Filtering)
|
||||||
|
.count(bindless_slab_resource_limit)
|
||||||
|
.build(
|
||||||
|
1,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
sampler(SamplerBindingType::NonFiltering)
|
||||||
|
.count(bindless_slab_resource_limit)
|
||||||
|
.build(
|
||||||
|
2,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
sampler(SamplerBindingType::Comparison)
|
||||||
|
.count(bindless_slab_resource_limit)
|
||||||
|
.build(
|
||||||
|
3,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
texture_1d(TextureSampleType::Float { filterable: true })
|
||||||
|
.count(bindless_slab_resource_limit)
|
||||||
|
.build(
|
||||||
|
4,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
texture_2d(TextureSampleType::Float { filterable: true })
|
||||||
|
.count(bindless_slab_resource_limit)
|
||||||
|
.build(
|
||||||
|
5,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
texture_2d_array(TextureSampleType::Float { filterable: true })
|
||||||
|
.count(bindless_slab_resource_limit)
|
||||||
|
.build(
|
||||||
|
6,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
texture_3d(TextureSampleType::Float { filterable: true })
|
||||||
|
.count(bindless_slab_resource_limit)
|
||||||
|
.build(
|
||||||
|
7,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
texture_cube(TextureSampleType::Float { filterable: true })
|
||||||
|
.count(bindless_slab_resource_limit)
|
||||||
|
.build(
|
||||||
|
8,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
texture_cube_array(TextureSampleType::Float { filterable: true })
|
||||||
|
.count(bindless_slab_resource_limit)
|
||||||
|
.build(
|
||||||
|
9,
|
||||||
|
ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindlessSlabResourceLimit {
|
||||||
|
/// Determines the actual bindless slab resource limit on this platform.
|
||||||
|
pub fn resolve(&self) -> u32 {
|
||||||
|
match *self {
|
||||||
|
BindlessSlabResourceLimit::Auto => AUTO_BINDLESS_SLAB_RESOURCE_LIMIT,
|
||||||
|
BindlessSlabResourceLimit::Custom(limit) => limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindlessResourceType {
|
||||||
|
/// Returns the binding number for the common array of this resource type.
|
||||||
|
///
|
||||||
|
/// For example, if you pass `BindlessResourceType::Texture2d`, this will
|
||||||
|
/// return 5, in order to match the `@group(2) @binding(5) var
|
||||||
|
/// bindless_textures_2d: binding_array<texture_2d<f32>>` declaration in
|
||||||
|
/// `bindless.wgsl`.
|
||||||
|
///
|
||||||
|
/// Not all resource types have fixed binding numbers. If you call
|
||||||
|
/// [`Self::binding_number`] on such a resource type, it returns `None`.
|
||||||
|
///
|
||||||
|
/// Note that this returns a static reference to the binding number, not the
|
||||||
|
/// binding number itself. This is to conform to an idiosyncratic API in
|
||||||
|
/// `wgpu` whereby binding numbers for binding arrays are taken by `&u32`
|
||||||
|
/// *reference*, not by `u32` value.
|
||||||
|
pub fn binding_number(&self) -> Option<&'static BindingNumber> {
|
||||||
|
match BINDING_NUMBERS.binary_search_by_key(self, |(key, _)| *key) {
|
||||||
|
Ok(binding_number) => Some(&BINDING_NUMBERS[binding_number].1),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TextureViewDimension> for BindlessResourceType {
|
||||||
|
fn from(texture_view_dimension: TextureViewDimension) -> Self {
|
||||||
|
match texture_view_dimension {
|
||||||
|
TextureViewDimension::D1 => BindlessResourceType::Texture1d,
|
||||||
|
TextureViewDimension::D2 => BindlessResourceType::Texture2d,
|
||||||
|
TextureViewDimension::D2Array => BindlessResourceType::Texture2dArray,
|
||||||
|
TextureViewDimension::Cube => BindlessResourceType::TextureCube,
|
||||||
|
TextureViewDimension::CubeArray => BindlessResourceType::TextureCubeArray,
|
||||||
|
TextureViewDimension::D3 => BindlessResourceType::Texture3d,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SamplerBindingType> for BindlessResourceType {
|
||||||
|
fn from(sampler_binding_type: SamplerBindingType) -> Self {
|
||||||
|
match sampler_binding_type {
|
||||||
|
SamplerBindingType::Filtering => BindlessResourceType::SamplerFiltering,
|
||||||
|
SamplerBindingType::NonFiltering => BindlessResourceType::SamplerNonFiltering,
|
||||||
|
SamplerBindingType::Comparison => BindlessResourceType::SamplerComparison,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for BindlessIndex {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for BindingNumber {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
70
third_party/bevy_render/src/render_resource/buffer.rs
vendored
Normal file
70
third_party/bevy_render/src/render_resource/buffer.rs
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use crate::define_atomic_id;
|
||||||
|
use crate::renderer::WgpuWrapper;
|
||||||
|
use core::ops::{Deref, RangeBounds};
|
||||||
|
|
||||||
|
define_atomic_id!(BufferId);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Buffer {
|
||||||
|
id: BufferId,
|
||||||
|
value: WgpuWrapper<wgpu::Buffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Buffer {
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> BufferId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slice(&self, bounds: impl RangeBounds<wgpu::BufferAddress>) -> BufferSlice<'_> {
|
||||||
|
BufferSlice {
|
||||||
|
id: self.id,
|
||||||
|
value: self.value.slice(bounds),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn unmap(&self) {
|
||||||
|
self.value.unmap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wgpu::Buffer> for Buffer {
|
||||||
|
fn from(value: wgpu::Buffer) -> Self {
|
||||||
|
Buffer {
|
||||||
|
id: BufferId::new(),
|
||||||
|
value: WgpuWrapper::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Buffer {
|
||||||
|
type Target = wgpu::Buffer;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BufferSlice<'a> {
|
||||||
|
id: BufferId,
|
||||||
|
value: wgpu::BufferSlice<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BufferSlice<'a> {
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> BufferId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deref for BufferSlice<'a> {
|
||||||
|
type Target = wgpu::BufferSlice<'a>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
587
third_party/bevy_render/src/render_resource/buffer_vec.rs
vendored
Normal file
587
third_party/bevy_render/src/render_resource/buffer_vec.rs
vendored
Normal file
@ -0,0 +1,587 @@
|
|||||||
|
use core::{iter, marker::PhantomData};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
render_resource::Buffer,
|
||||||
|
renderer::{RenderDevice, RenderQueue},
|
||||||
|
};
|
||||||
|
use bytemuck::{must_cast_slice, NoUninit};
|
||||||
|
use encase::{
|
||||||
|
internal::{WriteInto, Writer},
|
||||||
|
ShaderType,
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
use wgpu::{BindingResource, BufferAddress, BufferUsages};
|
||||||
|
|
||||||
|
use super::GpuArrayBufferable;
|
||||||
|
|
||||||
|
/// A structure for storing raw bytes that have already been properly formatted
|
||||||
|
/// for use by the GPU.
|
||||||
|
///
|
||||||
|
/// "Properly formatted" means that item data already meets the alignment and padding
|
||||||
|
/// requirements for how it will be used on the GPU. The item type must implement [`NoUninit`]
|
||||||
|
/// for its data representation to be directly copyable.
|
||||||
|
///
|
||||||
|
/// Index, vertex, and instance-rate vertex buffers have no alignment nor padding requirements and
|
||||||
|
/// so this helper type is a good choice for them.
|
||||||
|
///
|
||||||
|
/// The contained data is stored in system RAM. Calling [`reserve`](RawBufferVec::reserve)
|
||||||
|
/// allocates VRAM from the [`RenderDevice`].
|
||||||
|
/// [`write_buffer`](RawBufferVec::write_buffer) queues copying of the data
|
||||||
|
/// from system RAM to VRAM.
|
||||||
|
///
|
||||||
|
/// Other options for storing GPU-accessible data are:
|
||||||
|
/// * [`BufferVec`]
|
||||||
|
/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer)
|
||||||
|
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
|
||||||
|
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
|
||||||
|
/// * [`StorageBuffer`](crate::render_resource::StorageBuffer)
|
||||||
|
/// * [`Texture`](crate::render_resource::Texture)
|
||||||
|
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
|
||||||
|
pub struct RawBufferVec<T: NoUninit> {
|
||||||
|
values: Vec<T>,
|
||||||
|
buffer: Option<Buffer>,
|
||||||
|
capacity: usize,
|
||||||
|
item_size: usize,
|
||||||
|
buffer_usage: BufferUsages,
|
||||||
|
label: Option<String>,
|
||||||
|
changed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: NoUninit> RawBufferVec<T> {
|
||||||
|
/// Creates a new [`RawBufferVec`] with the given [`BufferUsages`].
|
||||||
|
pub const fn new(buffer_usage: BufferUsages) -> Self {
|
||||||
|
Self {
|
||||||
|
values: Vec::new(),
|
||||||
|
buffer: None,
|
||||||
|
capacity: 0,
|
||||||
|
item_size: size_of::<T>(),
|
||||||
|
buffer_usage,
|
||||||
|
label: None,
|
||||||
|
changed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a handle to the buffer, if the data has been uploaded.
|
||||||
|
#[inline]
|
||||||
|
pub fn buffer(&self) -> Option<&Buffer> {
|
||||||
|
self.buffer.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the binding for the buffer if the data has been uploaded.
|
||||||
|
#[inline]
|
||||||
|
pub fn binding(&self) -> Option<BindingResource<'_>> {
|
||||||
|
Some(BindingResource::Buffer(
|
||||||
|
self.buffer()?.as_entire_buffer_binding(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the amount of space that the GPU will use before reallocating.
|
||||||
|
#[inline]
|
||||||
|
pub fn capacity(&self) -> usize {
|
||||||
|
self.capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of items that have been pushed to this buffer.
|
||||||
|
#[inline]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.values.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the buffer is empty.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.values.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new value and returns its index.
|
||||||
|
pub fn push(&mut self, value: T) -> usize {
|
||||||
|
let index = self.values.len();
|
||||||
|
self.values.push(value);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(&mut self, other: &mut RawBufferVec<T>) {
|
||||||
|
self.values.append(&mut other.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the value at the given index.
|
||||||
|
pub fn get(&self, index: u32) -> Option<&T> {
|
||||||
|
self.values.get(index as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the value at the given index.
|
||||||
|
///
|
||||||
|
/// The index must be less than [`RawBufferVec::len`].
|
||||||
|
pub fn set(&mut self, index: u32, value: T) {
|
||||||
|
self.values[index as usize] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preallocates space for `count` elements in the internal CPU-side buffer.
|
||||||
|
///
|
||||||
|
/// Unlike [`RawBufferVec::reserve`], this doesn't have any effect on the GPU buffer.
|
||||||
|
pub fn reserve_internal(&mut self, count: usize) {
|
||||||
|
self.values.reserve(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the debugging label of the buffer.
|
||||||
|
///
|
||||||
|
/// The next time the buffer is updated (via [`reserve`](Self::reserve)), Bevy will inform
|
||||||
|
/// the driver of the new label.
|
||||||
|
pub fn set_label(&mut self, label: Option<&str>) {
|
||||||
|
let label = label.map(str::to_string);
|
||||||
|
|
||||||
|
if label != self.label {
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the label
|
||||||
|
pub fn get_label(&self) -> Option<&str> {
|
||||||
|
self.label.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Buffer`] on the [`RenderDevice`] with size
|
||||||
|
/// at least `size_of::<T>() * capacity`, unless a such a buffer already exists.
|
||||||
|
///
|
||||||
|
/// If a [`Buffer`] exists, but is too small, references to it will be discarded,
|
||||||
|
/// and a new [`Buffer`] will be created. Any previously created [`Buffer`]s
|
||||||
|
/// that are no longer referenced will be deleted by the [`RenderDevice`]
|
||||||
|
/// once it is done using them (typically 1-2 frames).
|
||||||
|
///
|
||||||
|
/// In addition to any [`BufferUsages`] provided when
|
||||||
|
/// the `RawBufferVec` was created, the buffer on the [`RenderDevice`]
|
||||||
|
/// is marked as [`BufferUsages::COPY_DST`](BufferUsages).
|
||||||
|
pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) {
|
||||||
|
let size = self.item_size * capacity;
|
||||||
|
if capacity > self.capacity || (self.changed && size > 0) {
|
||||||
|
self.capacity = capacity;
|
||||||
|
self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: self.label.as_deref(),
|
||||||
|
size: size as BufferAddress,
|
||||||
|
usage: BufferUsages::COPY_DST | self.buffer_usage,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
}));
|
||||||
|
self.changed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queues writing of data from system RAM to VRAM using the [`RenderDevice`]
|
||||||
|
/// and the provided [`RenderQueue`].
|
||||||
|
///
|
||||||
|
/// Before queuing the write, a [`reserve`](RawBufferVec::reserve) operation
|
||||||
|
/// is executed.
|
||||||
|
pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) {
|
||||||
|
if self.values.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.reserve(self.values.len(), device);
|
||||||
|
if let Some(buffer) = &self.buffer {
|
||||||
|
let range = 0..self.item_size * self.values.len();
|
||||||
|
let bytes: &[u8] = must_cast_slice(&self.values);
|
||||||
|
queue.write_buffer(buffer, 0, &bytes[range]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queues writing of data from system RAM to VRAM using the [`RenderDevice`]
|
||||||
|
/// and the provided [`RenderQueue`].
|
||||||
|
///
|
||||||
|
/// If the buffer is not initialized on the GPU or the range is bigger than the capacity it will
|
||||||
|
/// return an error. You'll need to either reserve a new buffer which will lose data on the GPU
|
||||||
|
/// or create a new buffer and copy the old data to it.
|
||||||
|
///
|
||||||
|
/// This will only write the data contained in the given range. It is useful if you only want
|
||||||
|
/// to update a part of the buffer.
|
||||||
|
pub fn write_buffer_range(
|
||||||
|
&mut self,
|
||||||
|
render_queue: &RenderQueue,
|
||||||
|
range: core::ops::Range<usize>,
|
||||||
|
) -> Result<(), WriteBufferRangeError> {
|
||||||
|
if self.values.is_empty() {
|
||||||
|
return Err(WriteBufferRangeError::NoValuesToUpload);
|
||||||
|
}
|
||||||
|
if range.end > self.item_size * self.capacity {
|
||||||
|
return Err(WriteBufferRangeError::RangeBiggerThanBuffer);
|
||||||
|
}
|
||||||
|
if let Some(buffer) = &self.buffer {
|
||||||
|
// Cast only the bytes we need to write
|
||||||
|
let bytes: &[u8] = must_cast_slice(&self.values[range.start..range.end]);
|
||||||
|
render_queue.write_buffer(buffer, (range.start * self.item_size) as u64, bytes);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(WriteBufferRangeError::BufferNotInitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reduces the length of the buffer.
|
||||||
|
pub fn truncate(&mut self, len: usize) {
|
||||||
|
self.values.truncate(len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all elements from the buffer.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.values.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes and returns the last element in the buffer.
|
||||||
|
pub fn pop(&mut self) -> Option<T> {
|
||||||
|
self.values.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn values(&self) -> &Vec<T> {
|
||||||
|
&self.values
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn values_mut(&mut self) -> &mut Vec<T> {
|
||||||
|
&mut self.values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> RawBufferVec<T>
|
||||||
|
where
|
||||||
|
T: NoUninit + Default,
|
||||||
|
{
|
||||||
|
pub fn grow_set(&mut self, index: u32, value: T) {
|
||||||
|
while index as usize + 1 > self.len() {
|
||||||
|
self.values.push(T::default());
|
||||||
|
}
|
||||||
|
self.values[index as usize] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: NoUninit> Extend<T> for RawBufferVec<T> {
|
||||||
|
#[inline]
|
||||||
|
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
|
||||||
|
self.values.extend(iter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`RawBufferVec`], but doesn't require that the data type `T` be
|
||||||
|
/// [`NoUninit`].
|
||||||
|
///
|
||||||
|
/// This is a high-performance data structure that you should use whenever
|
||||||
|
/// possible if your data is more complex than is suitable for [`RawBufferVec`].
|
||||||
|
/// The [`ShaderType`] trait from the `encase` library is used to ensure that
|
||||||
|
/// the data is correctly aligned for use by the GPU.
|
||||||
|
///
|
||||||
|
/// For performance reasons, unlike [`RawBufferVec`], this type doesn't allow
|
||||||
|
/// CPU access to the data after it's been added via [`BufferVec::push`]. If you
|
||||||
|
/// need CPU access to the data, consider another type, such as
|
||||||
|
/// [`StorageBuffer`][super::StorageBuffer].
|
||||||
|
///
|
||||||
|
/// Other options for storing GPU-accessible data are:
|
||||||
|
/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer)
|
||||||
|
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
|
||||||
|
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
|
||||||
|
/// * [`RawBufferVec`]
|
||||||
|
/// * [`StorageBuffer`](crate::render_resource::StorageBuffer)
|
||||||
|
/// * [`Texture`](crate::render_resource::Texture)
|
||||||
|
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
|
||||||
|
pub struct BufferVec<T>
|
||||||
|
where
|
||||||
|
T: ShaderType + WriteInto,
|
||||||
|
{
|
||||||
|
data: Vec<u8>,
|
||||||
|
buffer: Option<Buffer>,
|
||||||
|
capacity: usize,
|
||||||
|
buffer_usage: BufferUsages,
|
||||||
|
label: Option<String>,
|
||||||
|
label_changed: bool,
|
||||||
|
phantom: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> BufferVec<T>
|
||||||
|
where
|
||||||
|
T: ShaderType + WriteInto,
|
||||||
|
{
|
||||||
|
/// Creates a new [`BufferVec`] with the given [`BufferUsages`].
|
||||||
|
pub const fn new(buffer_usage: BufferUsages) -> Self {
|
||||||
|
Self {
|
||||||
|
data: vec![],
|
||||||
|
buffer: None,
|
||||||
|
capacity: 0,
|
||||||
|
buffer_usage,
|
||||||
|
label: None,
|
||||||
|
label_changed: false,
|
||||||
|
phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a handle to the buffer, if the data has been uploaded.
|
||||||
|
#[inline]
|
||||||
|
pub fn buffer(&self) -> Option<&Buffer> {
|
||||||
|
self.buffer.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the binding for the buffer if the data has been uploaded.
|
||||||
|
#[inline]
|
||||||
|
pub fn binding(&self) -> Option<BindingResource<'_>> {
|
||||||
|
Some(BindingResource::Buffer(
|
||||||
|
self.buffer()?.as_entire_buffer_binding(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the amount of space that the GPU will use before reallocating.
|
||||||
|
#[inline]
|
||||||
|
pub fn capacity(&self) -> usize {
|
||||||
|
self.capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of items that have been pushed to this buffer.
|
||||||
|
#[inline]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.data.len() / u64::from(T::min_size()) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the buffer is empty.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.data.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new value and returns its index.
|
||||||
|
pub fn push(&mut self, value: T) -> usize {
|
||||||
|
let element_size = u64::from(T::min_size()) as usize;
|
||||||
|
let offset = self.data.len();
|
||||||
|
|
||||||
|
// TODO: Consider using unsafe code to push uninitialized, to prevent
|
||||||
|
// the zeroing. It shows up in profiles.
|
||||||
|
self.data.extend(iter::repeat_n(0, element_size));
|
||||||
|
|
||||||
|
// Take a slice of the new data for `write_into` to use. This is
|
||||||
|
// important: it hoists the bounds check up here so that the compiler
|
||||||
|
// can eliminate all the bounds checks that `write_into` will emit.
|
||||||
|
let mut dest = &mut self.data[offset..(offset + element_size)];
|
||||||
|
value.write_into(&mut Writer::new(&value, &mut dest, 0).unwrap());
|
||||||
|
|
||||||
|
offset / u64::from(T::min_size()) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the debugging label of the buffer.
|
||||||
|
///
|
||||||
|
/// The next time the buffer is updated (via [`Self::reserve`]), Bevy will inform
|
||||||
|
/// the driver of the new label.
|
||||||
|
pub fn set_label(&mut self, label: Option<&str>) {
|
||||||
|
let label = label.map(str::to_string);
|
||||||
|
|
||||||
|
if label != self.label {
|
||||||
|
self.label_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the label
|
||||||
|
pub fn get_label(&self) -> Option<&str> {
|
||||||
|
self.label.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Buffer`] on the [`RenderDevice`] with size
|
||||||
|
/// at least `size_of::<T>() * capacity`, unless such a buffer already exists.
|
||||||
|
///
|
||||||
|
/// If a [`Buffer`] exists, but is too small, references to it will be discarded,
|
||||||
|
/// and a new [`Buffer`] will be created. Any previously created [`Buffer`]s
|
||||||
|
/// that are no longer referenced will be deleted by the [`RenderDevice`]
|
||||||
|
/// once it is done using them (typically 1-2 frames).
|
||||||
|
///
|
||||||
|
/// In addition to any [`BufferUsages`] provided when
|
||||||
|
/// the `BufferVec` was created, the buffer on the [`RenderDevice`]
|
||||||
|
/// is marked as [`BufferUsages::COPY_DST`](BufferUsages).
|
||||||
|
pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) {
|
||||||
|
if capacity <= self.capacity && !self.label_changed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.capacity = capacity;
|
||||||
|
let size = u64::from(T::min_size()) as usize * capacity;
|
||||||
|
self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: self.label.as_deref(),
|
||||||
|
size: size as BufferAddress,
|
||||||
|
usage: BufferUsages::COPY_DST | self.buffer_usage,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
}));
|
||||||
|
self.label_changed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queues writing of data from system RAM to VRAM using the [`RenderDevice`]
|
||||||
|
/// and the provided [`RenderQueue`].
|
||||||
|
///
|
||||||
|
/// Before queuing the write, a [`reserve`](BufferVec::reserve) operation is
|
||||||
|
/// executed.
|
||||||
|
pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) {
|
||||||
|
if self.data.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reserve(self.data.len() / u64::from(T::min_size()) as usize, device);
|
||||||
|
|
||||||
|
let Some(buffer) = &self.buffer else { return };
|
||||||
|
queue.write_buffer(buffer, 0, &self.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queues writing of data from system RAM to VRAM using the [`RenderDevice`]
|
||||||
|
/// and the provided [`RenderQueue`].
|
||||||
|
///
|
||||||
|
/// If the buffer is not initialized on the GPU or the range is bigger than the capacity it will
|
||||||
|
/// return an error. You'll need to either reserve a new buffer which will lose data on the GPU
|
||||||
|
/// or create a new buffer and copy the old data to it.
|
||||||
|
///
|
||||||
|
/// This will only write the data contained in the given range. It is useful if you only want
|
||||||
|
/// to update a part of the buffer.
|
||||||
|
pub fn write_buffer_range(
|
||||||
|
&mut self,
|
||||||
|
render_queue: &RenderQueue,
|
||||||
|
range: core::ops::Range<usize>,
|
||||||
|
) -> Result<(), WriteBufferRangeError> {
|
||||||
|
if self.data.is_empty() {
|
||||||
|
return Err(WriteBufferRangeError::NoValuesToUpload);
|
||||||
|
}
|
||||||
|
let item_size = u64::from(T::min_size()) as usize;
|
||||||
|
if range.end > item_size * self.capacity {
|
||||||
|
return Err(WriteBufferRangeError::RangeBiggerThanBuffer);
|
||||||
|
}
|
||||||
|
if let Some(buffer) = &self.buffer {
|
||||||
|
let bytes = &self.data[range.start..range.end];
|
||||||
|
render_queue.write_buffer(buffer, (range.start * item_size) as u64, bytes);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(WriteBufferRangeError::BufferNotInitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reduces the length of the buffer.
|
||||||
|
pub fn truncate(&mut self, len: usize) {
|
||||||
|
self.data.truncate(u64::from(T::min_size()) as usize * len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all elements from the buffer.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.data.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like a [`BufferVec`], but only reserves space on the GPU for elements
|
||||||
|
/// instead of initializing them CPU-side.
|
||||||
|
///
|
||||||
|
/// This type is useful when you're accumulating "output slots" for a GPU
|
||||||
|
/// compute shader to write into.
|
||||||
|
///
|
||||||
|
/// The type `T` need not be [`NoUninit`], unlike [`RawBufferVec`]; it only has to
|
||||||
|
/// be [`GpuArrayBufferable`].
|
||||||
|
pub struct UninitBufferVec<T>
|
||||||
|
where
|
||||||
|
T: GpuArrayBufferable,
|
||||||
|
{
|
||||||
|
buffer: Option<Buffer>,
|
||||||
|
len: usize,
|
||||||
|
capacity: usize,
|
||||||
|
item_size: usize,
|
||||||
|
buffer_usage: BufferUsages,
|
||||||
|
label: Option<String>,
|
||||||
|
label_changed: bool,
|
||||||
|
phantom: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> UninitBufferVec<T>
|
||||||
|
where
|
||||||
|
T: GpuArrayBufferable,
|
||||||
|
{
|
||||||
|
/// Creates a new [`UninitBufferVec`] with the given [`BufferUsages`].
|
||||||
|
pub const fn new(buffer_usage: BufferUsages) -> Self {
|
||||||
|
Self {
|
||||||
|
len: 0,
|
||||||
|
buffer: None,
|
||||||
|
capacity: 0,
|
||||||
|
item_size: size_of::<T>(),
|
||||||
|
buffer_usage,
|
||||||
|
label: None,
|
||||||
|
label_changed: false,
|
||||||
|
phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the buffer, if allocated.
|
||||||
|
#[inline]
|
||||||
|
pub fn buffer(&self) -> Option<&Buffer> {
|
||||||
|
self.buffer.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the binding for the buffer if the data has been uploaded.
|
||||||
|
#[inline]
|
||||||
|
pub fn binding(&self) -> Option<BindingResource<'_>> {
|
||||||
|
Some(BindingResource::Buffer(
|
||||||
|
self.buffer()?.as_entire_buffer_binding(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserves space for one more element in the buffer and returns its index.
|
||||||
|
pub fn add(&mut self) -> usize {
|
||||||
|
self.add_multiple(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserves space for the given number of elements in the buffer and
|
||||||
|
/// returns the index of the first one.
|
||||||
|
pub fn add_multiple(&mut self, count: usize) -> usize {
|
||||||
|
let index = self.len;
|
||||||
|
self.len += count;
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if no elements have been added to this [`UninitBufferVec`].
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all elements from the buffer.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the length of the buffer.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Materializes the buffer on the GPU with space for `capacity` elements.
|
||||||
|
///
|
||||||
|
/// If the buffer is already big enough, this function doesn't reallocate
|
||||||
|
/// the buffer.
|
||||||
|
pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) {
|
||||||
|
if capacity <= self.capacity && !self.label_changed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.capacity = capacity;
|
||||||
|
let size = self.item_size * capacity;
|
||||||
|
self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: self.label.as_deref(),
|
||||||
|
size: size as BufferAddress,
|
||||||
|
usage: BufferUsages::COPY_DST | self.buffer_usage,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.label_changed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Materializes the buffer on the GPU, with an appropriate size for the
|
||||||
|
/// elements that have been pushed so far.
|
||||||
|
pub fn write_buffer(&mut self, device: &RenderDevice) {
|
||||||
|
if !self.is_empty() {
|
||||||
|
self.reserve(self.len, device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error returned when `write_buffer_range` fails
|
||||||
|
///
|
||||||
|
/// See [`RawBufferVec::write_buffer_range`] [`BufferVec::write_buffer_range`]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, Error)]
|
||||||
|
pub enum WriteBufferRangeError {
|
||||||
|
#[error("the range is bigger than the capacity of the buffer")]
|
||||||
|
RangeBiggerThanBuffer,
|
||||||
|
#[error("the gpu buffer is not initialized")]
|
||||||
|
BufferNotInitialized,
|
||||||
|
#[error("there are no values to upload")]
|
||||||
|
NoValuesToUpload,
|
||||||
|
}
|
||||||
116
third_party/bevy_render/src/render_resource/gpu_array_buffer.rs
vendored
Normal file
116
third_party/bevy_render/src/render_resource/gpu_array_buffer.rs
vendored
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use super::{
|
||||||
|
binding_types::{storage_buffer_read_only, uniform_buffer_sized},
|
||||||
|
BindGroupLayoutEntryBuilder, BufferVec,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
render_resource::batched_uniform_buffer::BatchedUniformBuffer,
|
||||||
|
renderer::{RenderDevice, RenderQueue},
|
||||||
|
};
|
||||||
|
use bevy_ecs::{prelude::Component, resource::Resource};
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
use encase::{private::WriteInto, ShaderSize, ShaderType};
|
||||||
|
use nonmax::NonMaxU32;
|
||||||
|
use wgpu::{BindingResource, BufferUsages, Limits};
|
||||||
|
|
||||||
|
/// Trait for types able to go in a [`GpuArrayBuffer`].
|
||||||
|
pub trait GpuArrayBufferable: ShaderType + ShaderSize + WriteInto + Clone {}
|
||||||
|
|
||||||
|
impl<T: ShaderType + ShaderSize + WriteInto + Clone> GpuArrayBufferable for T {}
|
||||||
|
|
||||||
|
/// Stores an array of elements to be transferred to the GPU and made accessible to shaders as a read-only array.
|
||||||
|
///
|
||||||
|
/// On platforms that support storage buffers, this is equivalent to
|
||||||
|
/// [`BufferVec<T>`]. Otherwise, this falls back to a dynamic offset
|
||||||
|
/// uniform buffer with the largest array of T that fits within a uniform buffer
|
||||||
|
/// binding (within reasonable limits).
|
||||||
|
///
|
||||||
|
/// Other options for storing GPU-accessible data are:
|
||||||
|
/// * [`BufferVec`]
|
||||||
|
/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer)
|
||||||
|
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
|
||||||
|
/// * [`RawBufferVec`](crate::render_resource::RawBufferVec)
|
||||||
|
/// * [`StorageBuffer`](crate::render_resource::StorageBuffer)
|
||||||
|
/// * [`Texture`](crate::render_resource::Texture)
|
||||||
|
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub enum GpuArrayBuffer<T: GpuArrayBufferable> {
|
||||||
|
Uniform(BatchedUniformBuffer<T>),
|
||||||
|
Storage(BufferVec<T>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: GpuArrayBufferable> GpuArrayBuffer<T> {
|
||||||
|
pub fn new(limits: &Limits) -> Self {
|
||||||
|
if limits.max_storage_buffers_per_shader_stage == 0 {
|
||||||
|
GpuArrayBuffer::Uniform(BatchedUniformBuffer::new(limits))
|
||||||
|
} else {
|
||||||
|
GpuArrayBuffer::Storage(BufferVec::new(BufferUsages::STORAGE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
match self {
|
||||||
|
GpuArrayBuffer::Uniform(buffer) => buffer.clear(),
|
||||||
|
GpuArrayBuffer::Storage(buffer) => buffer.clear(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, value: T) -> GpuArrayBufferIndex<T> {
|
||||||
|
match self {
|
||||||
|
GpuArrayBuffer::Uniform(buffer) => buffer.push(value),
|
||||||
|
GpuArrayBuffer::Storage(buffer) => {
|
||||||
|
let index = buffer.push(value) as u32;
|
||||||
|
GpuArrayBufferIndex {
|
||||||
|
index,
|
||||||
|
dynamic_offset: None,
|
||||||
|
element_type: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) {
|
||||||
|
match self {
|
||||||
|
GpuArrayBuffer::Uniform(buffer) => buffer.write_buffer(device, queue),
|
||||||
|
GpuArrayBuffer::Storage(buffer) => buffer.write_buffer(device, queue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binding_layout(limits: &Limits) -> BindGroupLayoutEntryBuilder {
|
||||||
|
if limits.max_storage_buffers_per_shader_stage == 0 {
|
||||||
|
uniform_buffer_sized(
|
||||||
|
true,
|
||||||
|
// BatchedUniformBuffer uses a MaxCapacityArray that is runtime-sized, so we use
|
||||||
|
// None here and let wgpu figure out the size.
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
storage_buffer_read_only::<T>(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binding(&self) -> Option<BindingResource<'_>> {
|
||||||
|
match self {
|
||||||
|
GpuArrayBuffer::Uniform(buffer) => buffer.binding(),
|
||||||
|
GpuArrayBuffer::Storage(buffer) => buffer.binding(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn batch_size(limits: &Limits) -> Option<u32> {
|
||||||
|
if limits.max_storage_buffers_per_shader_stage == 0 {
|
||||||
|
Some(BatchedUniformBuffer::<T>::batch_size(limits) as u32)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An index into a [`GpuArrayBuffer`] for a given element.
|
||||||
|
#[derive(Component, Clone)]
|
||||||
|
pub struct GpuArrayBufferIndex<T: GpuArrayBufferable> {
|
||||||
|
/// The index to use in a shader into the array.
|
||||||
|
pub index: u32,
|
||||||
|
/// The dynamic offset to use when setting the bind group in a pass.
|
||||||
|
/// Only used on platforms that don't support storage buffers.
|
||||||
|
pub dynamic_offset: Option<NonMaxU32>,
|
||||||
|
pub element_type: PhantomData<T>,
|
||||||
|
}
|
||||||
74
third_party/bevy_render/src/render_resource/mod.rs
vendored
Normal file
74
third_party/bevy_render/src/render_resource/mod.rs
vendored
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
mod batched_uniform_buffer;
|
||||||
|
mod bind_group;
|
||||||
|
mod bind_group_entries;
|
||||||
|
mod bind_group_layout;
|
||||||
|
mod bind_group_layout_entries;
|
||||||
|
mod bindless;
|
||||||
|
mod buffer;
|
||||||
|
mod buffer_vec;
|
||||||
|
mod gpu_array_buffer;
|
||||||
|
mod pipeline;
|
||||||
|
mod pipeline_cache;
|
||||||
|
mod pipeline_specializer;
|
||||||
|
pub mod resource_macros;
|
||||||
|
mod specializer;
|
||||||
|
mod storage_buffer;
|
||||||
|
mod texture;
|
||||||
|
mod uniform_buffer;
|
||||||
|
|
||||||
|
pub use bind_group::*;
|
||||||
|
pub use bind_group_entries::*;
|
||||||
|
pub use bind_group_layout::*;
|
||||||
|
pub use bind_group_layout_entries::*;
|
||||||
|
pub use bindless::*;
|
||||||
|
pub use buffer::*;
|
||||||
|
pub use buffer_vec::*;
|
||||||
|
pub use gpu_array_buffer::*;
|
||||||
|
pub use pipeline::*;
|
||||||
|
pub use pipeline_cache::*;
|
||||||
|
pub use pipeline_specializer::*;
|
||||||
|
pub use specializer::*;
|
||||||
|
pub use storage_buffer::*;
|
||||||
|
pub use texture::*;
|
||||||
|
pub use uniform_buffer::*;
|
||||||
|
|
||||||
|
// TODO: decide where re-exports should go
|
||||||
|
pub use wgpu::{
|
||||||
|
util::{
|
||||||
|
BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs,
|
||||||
|
TextureDataOrder,
|
||||||
|
},
|
||||||
|
AccelerationStructureFlags, AccelerationStructureGeometryFlags,
|
||||||
|
AccelerationStructureUpdateMode, AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock,
|
||||||
|
AstcChannel, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutEntry, BindingResource,
|
||||||
|
BindingType, Blas, BlasBuildEntry, BlasGeometries, BlasGeometrySizeDescriptors,
|
||||||
|
BlasTriangleGeometry, BlasTriangleGeometrySizeDescriptor, BlendComponent, BlendFactor,
|
||||||
|
BlendOperation, BlendState, BufferAddress, BufferAsyncError, BufferBinding, BufferBindingType,
|
||||||
|
BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder,
|
||||||
|
CommandEncoderDescriptor, CompareFunction, ComputePass, ComputePassDescriptor,
|
||||||
|
ComputePipelineDescriptor as RawComputePipelineDescriptor, CreateBlasDescriptor,
|
||||||
|
CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags, Extent3d, Face,
|
||||||
|
Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, FrontFace,
|
||||||
|
ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, MapMode, MultisampleState,
|
||||||
|
Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor,
|
||||||
|
PollType, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange,
|
||||||
|
RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor,
|
||||||
|
RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler,
|
||||||
|
SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource,
|
||||||
|
ShaderStages, StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, StoreOp,
|
||||||
|
TexelCopyBufferInfo, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect,
|
||||||
|
TextureDescriptor, TextureDimension, TextureFormat, TextureFormatFeatureFlags,
|
||||||
|
TextureFormatFeatures, TextureSampleType, TextureUsages, TextureView as WgpuTextureView,
|
||||||
|
TextureViewDescriptor, TextureViewDimension, Tlas, TlasInstance, VertexAttribute,
|
||||||
|
VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState,
|
||||||
|
VertexStepMode, COPY_BUFFER_ALIGNMENT,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod encase {
|
||||||
|
pub use bevy_encase_derive::ShaderType;
|
||||||
|
pub use encase::*;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use self::encase::{ShaderSize, ShaderType};
|
||||||
|
|
||||||
|
pub use naga::ShaderStage;
|
||||||
199
third_party/bevy_render/src/render_resource/pipeline.rs
vendored
Normal file
199
third_party/bevy_render/src/render_resource/pipeline.rs
vendored
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
use crate::define_atomic_id;
|
||||||
|
use crate::renderer::WgpuWrapper;
|
||||||
|
use alloc::borrow::Cow;
|
||||||
|
use bevy_asset::Handle;
|
||||||
|
use bevy_mesh::VertexBufferLayout;
|
||||||
|
use bevy_shader::{Shader, ShaderDefVal};
|
||||||
|
use core::iter;
|
||||||
|
use core::ops::Deref;
|
||||||
|
use thiserror::Error;
|
||||||
|
use wgpu::{
|
||||||
|
BindGroupLayoutEntry, ColorTargetState, DepthStencilState, MultisampleState, PrimitiveState,
|
||||||
|
PushConstantRange,
|
||||||
|
};
|
||||||
|
|
||||||
|
define_atomic_id!(RenderPipelineId);
|
||||||
|
|
||||||
|
/// A [`RenderPipeline`] represents a graphics pipeline and its stages (shaders), bindings and vertex buffers.
|
||||||
|
///
|
||||||
|
/// May be converted from and dereferences to a wgpu [`RenderPipeline`](wgpu::RenderPipeline).
|
||||||
|
/// Can be created via [`RenderDevice::create_render_pipeline`](crate::renderer::RenderDevice::create_render_pipeline).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct RenderPipeline {
|
||||||
|
id: RenderPipelineId,
|
||||||
|
value: WgpuWrapper<wgpu::RenderPipeline>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderPipeline {
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> RenderPipelineId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wgpu::RenderPipeline> for RenderPipeline {
|
||||||
|
fn from(value: wgpu::RenderPipeline) -> Self {
|
||||||
|
RenderPipeline {
|
||||||
|
id: RenderPipelineId::new(),
|
||||||
|
value: WgpuWrapper::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for RenderPipeline {
|
||||||
|
type Target = wgpu::RenderPipeline;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
define_atomic_id!(ComputePipelineId);
|
||||||
|
|
||||||
|
/// A [`ComputePipeline`] represents a compute pipeline and its single shader stage.
|
||||||
|
///
|
||||||
|
/// May be converted from and dereferences to a wgpu [`ComputePipeline`](wgpu::ComputePipeline).
|
||||||
|
/// Can be created via [`RenderDevice::create_compute_pipeline`](crate::renderer::RenderDevice::create_compute_pipeline).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ComputePipeline {
|
||||||
|
id: ComputePipelineId,
|
||||||
|
value: WgpuWrapper<wgpu::ComputePipeline>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComputePipeline {
|
||||||
|
/// Returns the [`ComputePipelineId`].
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> ComputePipelineId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wgpu::ComputePipeline> for ComputePipeline {
|
||||||
|
fn from(value: wgpu::ComputePipeline) -> Self {
|
||||||
|
ComputePipeline {
|
||||||
|
id: ComputePipelineId::new(),
|
||||||
|
value: WgpuWrapper::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for ComputePipeline {
|
||||||
|
type Target = wgpu::ComputePipeline;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct BindGroupLayoutDescriptor {
|
||||||
|
/// Debug label of the bind group layout descriptor. This will show up in graphics debuggers for easy identification.
|
||||||
|
pub label: Cow<'static, str>,
|
||||||
|
pub entries: Vec<BindGroupLayoutEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindGroupLayoutDescriptor {
|
||||||
|
pub fn new(label: impl Into<Cow<'static, str>>, entries: &[BindGroupLayoutEntry]) -> Self {
|
||||||
|
Self {
|
||||||
|
label: label.into(),
|
||||||
|
entries: entries.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a render (graphics) pipeline.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Default)]
|
||||||
|
pub struct RenderPipelineDescriptor {
|
||||||
|
/// Debug label of the pipeline. This will show up in graphics debuggers for easy identification.
|
||||||
|
pub label: Option<Cow<'static, str>>,
|
||||||
|
/// The layout of bind groups for this pipeline.
|
||||||
|
pub layout: Vec<BindGroupLayoutDescriptor>,
|
||||||
|
/// The push constant ranges for this pipeline.
|
||||||
|
/// Supply an empty vector if the pipeline doesn't use push constants.
|
||||||
|
pub push_constant_ranges: Vec<PushConstantRange>,
|
||||||
|
/// The compiled vertex stage, its entry point, and the input buffers layout.
|
||||||
|
pub vertex: VertexState,
|
||||||
|
/// The properties of the pipeline at the primitive assembly and rasterization level.
|
||||||
|
pub primitive: PrimitiveState,
|
||||||
|
/// The effect of draw calls on the depth and stencil aspects of the output target, if any.
|
||||||
|
pub depth_stencil: Option<DepthStencilState>,
|
||||||
|
/// The multi-sampling properties of the pipeline.
|
||||||
|
pub multisample: MultisampleState,
|
||||||
|
/// The compiled fragment stage, its entry point, and the color targets.
|
||||||
|
pub fragment: Option<FragmentState>,
|
||||||
|
/// Whether to zero-initialize workgroup memory by default. If you're not sure, set this to true.
|
||||||
|
/// If this is false, reading from workgroup variables before writing to them will result in garbage values.
|
||||||
|
pub zero_initialize_workgroup_memory: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Error)]
|
||||||
|
#[error("RenderPipelineDescriptor has no FragmentState configured")]
|
||||||
|
pub struct NoFragmentStateError;
|
||||||
|
|
||||||
|
impl RenderPipelineDescriptor {
|
||||||
|
pub fn fragment_mut(&mut self) -> Result<&mut FragmentState, NoFragmentStateError> {
|
||||||
|
self.fragment.as_mut().ok_or(NoFragmentStateError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_layout(&mut self, index: usize, layout: BindGroupLayoutDescriptor) {
|
||||||
|
filling_set_at(&mut self.layout, index, bevy_utils::default(), layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Default)]
|
||||||
|
pub struct VertexState {
|
||||||
|
/// The compiled shader module for this stage.
|
||||||
|
pub shader: Handle<Shader>,
|
||||||
|
pub shader_defs: Vec<ShaderDefVal>,
|
||||||
|
/// The name of the entry point in the compiled shader, or `None` if the default entry point
|
||||||
|
/// is used.
|
||||||
|
pub entry_point: Option<Cow<'static, str>>,
|
||||||
|
/// The format of any vertex buffers used with this pipeline.
|
||||||
|
pub buffers: Vec<VertexBufferLayout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the fragment process in a render pipeline.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub struct FragmentState {
|
||||||
|
/// The compiled shader module for this stage.
|
||||||
|
pub shader: Handle<Shader>,
|
||||||
|
pub shader_defs: Vec<ShaderDefVal>,
|
||||||
|
/// The name of the entry point in the compiled shader, or `None` if the default entry point
|
||||||
|
/// is used.
|
||||||
|
pub entry_point: Option<Cow<'static, str>>,
|
||||||
|
/// The color state of the render targets.
|
||||||
|
pub targets: Vec<Option<ColorTargetState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FragmentState {
|
||||||
|
pub fn set_target(&mut self, index: usize, target: ColorTargetState) {
|
||||||
|
filling_set_at(&mut self.targets, index, None, Some(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a compute pipeline.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub struct ComputePipelineDescriptor {
|
||||||
|
pub label: Option<Cow<'static, str>>,
|
||||||
|
pub layout: Vec<BindGroupLayoutDescriptor>,
|
||||||
|
pub push_constant_ranges: Vec<PushConstantRange>,
|
||||||
|
/// The compiled shader module for this stage.
|
||||||
|
pub shader: Handle<Shader>,
|
||||||
|
pub shader_defs: Vec<ShaderDefVal>,
|
||||||
|
/// The name of the entry point in the compiled shader, or `None` if the default entry point
|
||||||
|
/// is used.
|
||||||
|
pub entry_point: Option<Cow<'static, str>>,
|
||||||
|
/// Whether to zero-initialize workgroup memory by default. If you're not sure, set this to true.
|
||||||
|
/// If this is false, reading from workgroup variables before writing to them will result in garbage values.
|
||||||
|
pub zero_initialize_workgroup_memory: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility function to set a value at the specified index, extending with
|
||||||
|
// a filler value if the index is out of bounds.
|
||||||
|
fn filling_set_at<T: Clone>(vec: &mut Vec<T>, index: usize, filler: T, value: T) {
|
||||||
|
let num_to_fill = (index + 1).saturating_sub(vec.len());
|
||||||
|
vec.extend(iter::repeat_n(filler, num_to_fill));
|
||||||
|
vec[index] = value;
|
||||||
|
}
|
||||||
882
third_party/bevy_render/src/render_resource/pipeline_cache.rs
vendored
Normal file
882
third_party/bevy_render/src/render_resource/pipeline_cache.rs
vendored
Normal file
@ -0,0 +1,882 @@
|
|||||||
|
use crate::{
|
||||||
|
render_resource::*,
|
||||||
|
renderer::{RenderAdapter, RenderDevice, WgpuWrapper},
|
||||||
|
Extract,
|
||||||
|
};
|
||||||
|
use alloc::{borrow::Cow, sync::Arc};
|
||||||
|
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
|
||||||
|
use bevy_ecs::{
|
||||||
|
message::MessageReader,
|
||||||
|
resource::Resource,
|
||||||
|
system::{Res, ResMut},
|
||||||
|
};
|
||||||
|
use bevy_platform::collections::{HashMap, HashSet};
|
||||||
|
use bevy_shader::{
|
||||||
|
CachedPipelineId, PipelineCacheError, Shader, ShaderCache, ShaderCacheSource, ShaderDefVal,
|
||||||
|
ValidateShader,
|
||||||
|
};
|
||||||
|
use bevy_tasks::Task;
|
||||||
|
use bevy_utils::default;
|
||||||
|
use core::{future::Future, hash::Hash, mem};
|
||||||
|
use std::sync::{Mutex, PoisonError};
|
||||||
|
use tracing::error;
|
||||||
|
use wgpu::{PipelineCompilationOptions, VertexBufferLayout as RawVertexBufferLayout};
|
||||||
|
|
||||||
|
/// A descriptor for a [`Pipeline`].
|
||||||
|
///
|
||||||
|
/// Used to store a heterogenous collection of render and compute pipeline descriptors together.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PipelineDescriptor {
|
||||||
|
RenderPipelineDescriptor(Box<RenderPipelineDescriptor>),
|
||||||
|
ComputePipelineDescriptor(Box<ComputePipelineDescriptor>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pipeline defining the data layout and shader logic for a specific GPU task.
|
||||||
|
///
|
||||||
|
/// Used to store a heterogenous collection of render and compute pipelines together.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Pipeline {
|
||||||
|
RenderPipeline(RenderPipeline),
|
||||||
|
ComputePipeline(ComputePipeline),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Index of a cached render pipeline in a [`PipelineCache`].
|
||||||
|
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
|
pub struct CachedRenderPipelineId(CachedPipelineId);
|
||||||
|
|
||||||
|
impl CachedRenderPipelineId {
|
||||||
|
/// An invalid cached render pipeline index, often used to initialize a variable.
|
||||||
|
pub const INVALID: Self = CachedRenderPipelineId(usize::MAX);
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> usize {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Index of a cached compute pipeline in a [`PipelineCache`].
|
||||||
|
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
|
pub struct CachedComputePipelineId(CachedPipelineId);
|
||||||
|
|
||||||
|
impl CachedComputePipelineId {
|
||||||
|
/// An invalid cached compute pipeline index, often used to initialize a variable.
|
||||||
|
pub const INVALID: Self = CachedComputePipelineId(usize::MAX);
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> usize {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CachedPipeline {
|
||||||
|
pub descriptor: PipelineDescriptor,
|
||||||
|
pub state: CachedPipelineState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State of a cached pipeline inserted into a [`PipelineCache`].
|
||||||
|
#[cfg_attr(
|
||||||
|
not(target_arch = "wasm32"),
|
||||||
|
expect(
|
||||||
|
clippy::large_enum_variant,
|
||||||
|
reason = "See https://github.com/bevyengine/bevy/issues/19220"
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CachedPipelineState {
|
||||||
|
/// The pipeline GPU object is queued for creation.
|
||||||
|
Queued,
|
||||||
|
/// The pipeline GPU object is being created.
|
||||||
|
Creating(Task<Result<Pipeline, PipelineCacheError>>),
|
||||||
|
/// The pipeline GPU object was created successfully and is available (allocated on the GPU).
|
||||||
|
Ok(Pipeline),
|
||||||
|
/// An error occurred while trying to create the pipeline GPU object.
|
||||||
|
Err(PipelineCacheError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CachedPipelineState {
|
||||||
|
/// Convenience method to "unwrap" a pipeline state into its underlying GPU object.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The method returns the allocated pipeline GPU object.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This method panics if the pipeline GPU object is not available, either because it is
|
||||||
|
/// pending creation or because an error occurred while attempting to create GPU object.
|
||||||
|
pub fn unwrap(&self) -> &Pipeline {
|
||||||
|
match self {
|
||||||
|
CachedPipelineState::Ok(pipeline) => pipeline,
|
||||||
|
CachedPipelineState::Queued => {
|
||||||
|
panic!("Pipeline has not been compiled yet. It is still in the 'Queued' state.")
|
||||||
|
}
|
||||||
|
CachedPipelineState::Creating(..) => {
|
||||||
|
panic!("Pipeline has not been compiled yet. It is still in the 'Creating' state.")
|
||||||
|
}
|
||||||
|
CachedPipelineState::Err(err) => panic!("{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayoutCacheKey = (Vec<BindGroupLayoutId>, Vec<PushConstantRange>);
|
||||||
|
#[derive(Default)]
|
||||||
|
struct LayoutCache {
|
||||||
|
layouts: HashMap<LayoutCacheKey, Arc<WgpuWrapper<PipelineLayout>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutCache {
|
||||||
|
fn get(
|
||||||
|
&mut self,
|
||||||
|
render_device: &RenderDevice,
|
||||||
|
bind_group_layouts: &[BindGroupLayout],
|
||||||
|
push_constant_ranges: Vec<PushConstantRange>,
|
||||||
|
) -> Arc<WgpuWrapper<PipelineLayout>> {
|
||||||
|
let bind_group_ids = bind_group_layouts.iter().map(BindGroupLayout::id).collect();
|
||||||
|
self.layouts
|
||||||
|
.entry((bind_group_ids, push_constant_ranges))
|
||||||
|
.or_insert_with_key(|(_, push_constant_ranges)| {
|
||||||
|
let bind_group_layouts = bind_group_layouts
|
||||||
|
.iter()
|
||||||
|
.map(BindGroupLayout::value)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Arc::new(WgpuWrapper::new(render_device.create_pipeline_layout(
|
||||||
|
&PipelineLayoutDescriptor {
|
||||||
|
bind_group_layouts: &bind_group_layouts,
|
||||||
|
push_constant_ranges,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[expect(
|
||||||
|
clippy::result_large_err,
|
||||||
|
reason = "See https://github.com/bevyengine/bevy/issues/19220"
|
||||||
|
)]
|
||||||
|
fn load_module(
|
||||||
|
render_device: &RenderDevice,
|
||||||
|
shader_source: ShaderCacheSource,
|
||||||
|
validate_shader: &ValidateShader,
|
||||||
|
) -> Result<WgpuWrapper<ShaderModule>, PipelineCacheError> {
|
||||||
|
let shader_source = match shader_source {
|
||||||
|
#[cfg(feature = "shader_format_spirv")]
|
||||||
|
ShaderCacheSource::SpirV(data) => wgpu::util::make_spirv(data),
|
||||||
|
#[cfg(not(feature = "shader_format_spirv"))]
|
||||||
|
ShaderCacheSource::SpirV(_) => {
|
||||||
|
unimplemented!("Enable feature \"shader_format_spirv\" to use SPIR-V shaders")
|
||||||
|
}
|
||||||
|
ShaderCacheSource::Wgsl(src) => ShaderSource::Wgsl(Cow::Owned(src)),
|
||||||
|
#[cfg(not(feature = "decoupled_naga"))]
|
||||||
|
ShaderCacheSource::Naga(src) => ShaderSource::Naga(Cow::Owned(src)),
|
||||||
|
};
|
||||||
|
let module_descriptor = ShaderModuleDescriptor {
|
||||||
|
label: None,
|
||||||
|
source: shader_source,
|
||||||
|
};
|
||||||
|
|
||||||
|
render_device
|
||||||
|
.wgpu_device()
|
||||||
|
.push_error_scope(wgpu::ErrorFilter::Validation);
|
||||||
|
|
||||||
|
let shader_module = WgpuWrapper::new(match validate_shader {
|
||||||
|
ValidateShader::Enabled => {
|
||||||
|
render_device.create_and_validate_shader_module(module_descriptor)
|
||||||
|
}
|
||||||
|
// SAFETY: we are interfacing with shader code, which may contain undefined behavior,
|
||||||
|
// such as indexing out of bounds.
|
||||||
|
// The checks required are prohibitively expensive and a poor default for game engines.
|
||||||
|
ValidateShader::Disabled => unsafe {
|
||||||
|
render_device.create_shader_module(module_descriptor)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let error = render_device.wgpu_device().pop_error_scope();
|
||||||
|
|
||||||
|
// `now_or_never` will return Some if the future is ready and None otherwise.
|
||||||
|
// On native platforms, wgpu will yield the error immediately while on wasm it may take longer since the browser APIs are asynchronous.
|
||||||
|
// So to keep the complexity of the ShaderCache low, we will only catch this error early on native platforms,
|
||||||
|
// and on wasm the error will be handled by wgpu and crash the application.
|
||||||
|
if let Some(Some(wgpu::Error::Validation { description, .. })) =
|
||||||
|
bevy_tasks::futures::now_or_never(error)
|
||||||
|
{
|
||||||
|
return Err(PipelineCacheError::CreateShaderModule(description));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(shader_module)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct BindGroupLayoutCache {
|
||||||
|
bgls: HashMap<BindGroupLayoutDescriptor, BindGroupLayout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BindGroupLayoutCache {
|
||||||
|
fn get(
|
||||||
|
&mut self,
|
||||||
|
render_device: &RenderDevice,
|
||||||
|
descriptor: BindGroupLayoutDescriptor,
|
||||||
|
) -> BindGroupLayout {
|
||||||
|
self.bgls
|
||||||
|
.entry(descriptor)
|
||||||
|
.or_insert_with_key(|descriptor| {
|
||||||
|
render_device
|
||||||
|
.create_bind_group_layout(descriptor.label.as_ref(), &descriptor.entries)
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cache for render and compute pipelines.
|
||||||
|
///
|
||||||
|
/// The cache stores existing render and compute pipelines allocated on the GPU, as well as
|
||||||
|
/// pending creation. Pipelines inserted into the cache are identified by a unique ID, which
|
||||||
|
/// can be used to retrieve the actual GPU object once it's ready. The creation of the GPU
|
||||||
|
/// pipeline object is deferred to the [`RenderSystems::Render`] step, just before the render
|
||||||
|
/// graph starts being processed, as this requires access to the GPU.
|
||||||
|
///
|
||||||
|
/// Note that the cache does not perform automatic deduplication of identical pipelines. It is
|
||||||
|
/// up to the user not to insert the same pipeline twice to avoid wasting GPU resources.
|
||||||
|
///
|
||||||
|
/// [`RenderSystems::Render`]: crate::RenderSystems::Render
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct PipelineCache {
|
||||||
|
layout_cache: Arc<Mutex<LayoutCache>>,
|
||||||
|
bindgroup_layout_cache: Arc<Mutex<BindGroupLayoutCache>>,
|
||||||
|
shader_cache: Arc<Mutex<ShaderCache<WgpuWrapper<ShaderModule>, RenderDevice>>>,
|
||||||
|
device: RenderDevice,
|
||||||
|
pipelines: Vec<CachedPipeline>,
|
||||||
|
waiting_pipelines: HashSet<CachedPipelineId>,
|
||||||
|
new_pipelines: Mutex<Vec<CachedPipeline>>,
|
||||||
|
global_shader_defs: Vec<ShaderDefVal>,
|
||||||
|
/// If `true`, disables asynchronous pipeline compilation.
|
||||||
|
/// This has no effect on macOS, wasm, or without the `multi_threaded` feature.
|
||||||
|
synchronous_pipeline_compilation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PipelineCache {
|
||||||
|
/// Returns an iterator over the pipelines in the pipeline cache.
|
||||||
|
pub fn pipelines(&self) -> impl Iterator<Item = &CachedPipeline> {
|
||||||
|
self.pipelines.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a iterator of the IDs of all currently waiting pipelines.
|
||||||
|
pub fn waiting_pipelines(&self) -> impl Iterator<Item = CachedPipelineId> + '_ {
|
||||||
|
self.waiting_pipelines.iter().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new pipeline cache associated with the given render device.
|
||||||
|
pub fn new(
|
||||||
|
device: RenderDevice,
|
||||||
|
render_adapter: RenderAdapter,
|
||||||
|
synchronous_pipeline_compilation: bool,
|
||||||
|
) -> Self {
|
||||||
|
let mut global_shader_defs = Vec::new();
|
||||||
|
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
|
||||||
|
{
|
||||||
|
global_shader_defs.push("NO_ARRAY_TEXTURES_SUPPORT".into());
|
||||||
|
global_shader_defs.push("NO_CUBE_ARRAY_TEXTURES_SUPPORT".into());
|
||||||
|
global_shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(target_abi = "sim") {
|
||||||
|
global_shader_defs.push("NO_CUBE_ARRAY_TEXTURES_SUPPORT".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
global_shader_defs.push(ShaderDefVal::UInt(
|
||||||
|
String::from("AVAILABLE_STORAGE_BUFFER_BINDINGS"),
|
||||||
|
device.limits().max_storage_buffers_per_shader_stage,
|
||||||
|
));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
shader_cache: Arc::new(Mutex::new(ShaderCache::new(
|
||||||
|
device.features(),
|
||||||
|
render_adapter.get_downlevel_capabilities().flags,
|
||||||
|
load_module,
|
||||||
|
))),
|
||||||
|
device,
|
||||||
|
layout_cache: default(),
|
||||||
|
bindgroup_layout_cache: default(),
|
||||||
|
waiting_pipelines: default(),
|
||||||
|
new_pipelines: default(),
|
||||||
|
pipelines: default(),
|
||||||
|
global_shader_defs,
|
||||||
|
synchronous_pipeline_compilation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the state of a cached render pipeline.
|
||||||
|
///
|
||||||
|
/// See [`PipelineCache::queue_render_pipeline()`].
|
||||||
|
#[inline]
|
||||||
|
pub fn get_render_pipeline_state(&self, id: CachedRenderPipelineId) -> &CachedPipelineState {
|
||||||
|
// If the pipeline id isn't in `pipelines`, it's queued in `new_pipelines`
|
||||||
|
self.pipelines
|
||||||
|
.get(id.0)
|
||||||
|
.map_or(&CachedPipelineState::Queued, |pipeline| &pipeline.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the state of a cached compute pipeline.
|
||||||
|
///
|
||||||
|
/// See [`PipelineCache::queue_compute_pipeline()`].
|
||||||
|
#[inline]
|
||||||
|
pub fn get_compute_pipeline_state(&self, id: CachedComputePipelineId) -> &CachedPipelineState {
|
||||||
|
// If the pipeline id isn't in `pipelines`, it's queued in `new_pipelines`
|
||||||
|
self.pipelines
|
||||||
|
.get(id.0)
|
||||||
|
.map_or(&CachedPipelineState::Queued, |pipeline| &pipeline.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the render pipeline descriptor a cached render pipeline was inserted from.
|
||||||
|
///
|
||||||
|
/// See [`PipelineCache::queue_render_pipeline()`].
|
||||||
|
///
|
||||||
|
/// **Note**: Be careful calling this method. It will panic if called with a pipeline that
|
||||||
|
/// has been queued but has not yet been processed by [`PipelineCache::process_queue()`].
|
||||||
|
#[inline]
|
||||||
|
pub fn get_render_pipeline_descriptor(
|
||||||
|
&self,
|
||||||
|
id: CachedRenderPipelineId,
|
||||||
|
) -> &RenderPipelineDescriptor {
|
||||||
|
match &self.pipelines[id.0].descriptor {
|
||||||
|
PipelineDescriptor::RenderPipelineDescriptor(descriptor) => descriptor,
|
||||||
|
PipelineDescriptor::ComputePipelineDescriptor(_) => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the compute pipeline descriptor a cached render pipeline was inserted from.
|
||||||
|
///
|
||||||
|
/// See [`PipelineCache::queue_compute_pipeline()`].
|
||||||
|
///
|
||||||
|
/// **Note**: Be careful calling this method. It will panic if called with a pipeline that
|
||||||
|
/// has been queued but has not yet been processed by [`PipelineCache::process_queue()`].
|
||||||
|
#[inline]
|
||||||
|
pub fn get_compute_pipeline_descriptor(
|
||||||
|
&self,
|
||||||
|
id: CachedComputePipelineId,
|
||||||
|
) -> &ComputePipelineDescriptor {
|
||||||
|
match &self.pipelines[id.0].descriptor {
|
||||||
|
PipelineDescriptor::RenderPipelineDescriptor(_) => unreachable!(),
|
||||||
|
PipelineDescriptor::ComputePipelineDescriptor(descriptor) => descriptor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to retrieve a render pipeline GPU object from a cached ID.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// This method returns a successfully created render pipeline if any, or `None` if the pipeline
|
||||||
|
/// was not created yet or if there was an error during creation. You can check the actual creation
|
||||||
|
/// state with [`PipelineCache::get_render_pipeline_state()`].
|
||||||
|
#[inline]
|
||||||
|
pub fn get_render_pipeline(&self, id: CachedRenderPipelineId) -> Option<&RenderPipeline> {
|
||||||
|
if let CachedPipelineState::Ok(Pipeline::RenderPipeline(pipeline)) =
|
||||||
|
&self.pipelines.get(id.0)?.state
|
||||||
|
{
|
||||||
|
Some(pipeline)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for a render pipeline to finish compiling.
|
||||||
|
#[inline]
|
||||||
|
pub fn block_on_render_pipeline(&mut self, id: CachedRenderPipelineId) {
|
||||||
|
if self.pipelines.len() <= id.0 {
|
||||||
|
self.process_queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = &mut self.pipelines[id.0].state;
|
||||||
|
if let CachedPipelineState::Creating(task) = state {
|
||||||
|
*state = match bevy_tasks::block_on(task) {
|
||||||
|
Ok(p) => CachedPipelineState::Ok(p),
|
||||||
|
Err(e) => CachedPipelineState::Err(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to retrieve a compute pipeline GPU object from a cached ID.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// This method returns a successfully created compute pipeline if any, or `None` if the pipeline
|
||||||
|
/// was not created yet or if there was an error during creation. You can check the actual creation
|
||||||
|
/// state with [`PipelineCache::get_compute_pipeline_state()`].
|
||||||
|
#[inline]
|
||||||
|
pub fn get_compute_pipeline(&self, id: CachedComputePipelineId) -> Option<&ComputePipeline> {
|
||||||
|
if let CachedPipelineState::Ok(Pipeline::ComputePipeline(pipeline)) =
|
||||||
|
&self.pipelines.get(id.0)?.state
|
||||||
|
{
|
||||||
|
Some(pipeline)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a render pipeline into the cache, and queue its creation.
|
||||||
|
///
|
||||||
|
/// The pipeline is always inserted and queued for creation. There is no attempt to deduplicate it with
|
||||||
|
/// an already cached pipeline.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// This method returns the unique render shader ID of the cached pipeline, which can be used to query
|
||||||
|
/// the caching state with [`get_render_pipeline_state()`] and to retrieve the created GPU pipeline once
|
||||||
|
/// it's ready with [`get_render_pipeline()`].
|
||||||
|
///
|
||||||
|
/// [`get_render_pipeline_state()`]: PipelineCache::get_render_pipeline_state
|
||||||
|
/// [`get_render_pipeline()`]: PipelineCache::get_render_pipeline
|
||||||
|
pub fn queue_render_pipeline(
|
||||||
|
&self,
|
||||||
|
descriptor: RenderPipelineDescriptor,
|
||||||
|
) -> CachedRenderPipelineId {
|
||||||
|
let mut new_pipelines = self
|
||||||
|
.new_pipelines
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(PoisonError::into_inner);
|
||||||
|
let id = CachedRenderPipelineId(self.pipelines.len() + new_pipelines.len());
|
||||||
|
new_pipelines.push(CachedPipeline {
|
||||||
|
descriptor: PipelineDescriptor::RenderPipelineDescriptor(Box::new(descriptor)),
|
||||||
|
state: CachedPipelineState::Queued,
|
||||||
|
});
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a compute pipeline into the cache, and queue its creation.
|
||||||
|
///
|
||||||
|
/// The pipeline is always inserted and queued for creation. There is no attempt to deduplicate it with
|
||||||
|
/// an already cached pipeline.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// This method returns the unique compute shader ID of the cached pipeline, which can be used to query
|
||||||
|
/// the caching state with [`get_compute_pipeline_state()`] and to retrieve the created GPU pipeline once
|
||||||
|
/// it's ready with [`get_compute_pipeline()`].
|
||||||
|
///
|
||||||
|
/// [`get_compute_pipeline_state()`]: PipelineCache::get_compute_pipeline_state
|
||||||
|
/// [`get_compute_pipeline()`]: PipelineCache::get_compute_pipeline
|
||||||
|
pub fn queue_compute_pipeline(
|
||||||
|
&self,
|
||||||
|
descriptor: ComputePipelineDescriptor,
|
||||||
|
) -> CachedComputePipelineId {
|
||||||
|
let mut new_pipelines = self
|
||||||
|
.new_pipelines
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(PoisonError::into_inner);
|
||||||
|
let id = CachedComputePipelineId(self.pipelines.len() + new_pipelines.len());
|
||||||
|
new_pipelines.push(CachedPipeline {
|
||||||
|
descriptor: PipelineDescriptor::ComputePipelineDescriptor(Box::new(descriptor)),
|
||||||
|
state: CachedPipelineState::Queued,
|
||||||
|
});
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bind_group_layout(
|
||||||
|
&self,
|
||||||
|
bind_group_layout_descriptor: &BindGroupLayoutDescriptor,
|
||||||
|
) -> BindGroupLayout {
|
||||||
|
self.bindgroup_layout_cache
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(&self.device, bind_group_layout_descriptor.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a [`Shader`] into this cache with the provided [`AssetId`].
|
||||||
|
pub fn set_shader(&mut self, id: AssetId<Shader>, shader: Shader) {
|
||||||
|
let mut shader_cache = self.shader_cache.lock().unwrap();
|
||||||
|
let pipelines_to_queue = shader_cache.set_shader(id, shader);
|
||||||
|
for cached_pipeline in pipelines_to_queue {
|
||||||
|
self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;
|
||||||
|
self.waiting_pipelines.insert(cached_pipeline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a [`Shader`] from this cache if it exists.
|
||||||
|
pub fn remove_shader(&mut self, shader: AssetId<Shader>) {
|
||||||
|
let mut shader_cache = self.shader_cache.lock().unwrap();
|
||||||
|
let pipelines_to_queue = shader_cache.remove(shader);
|
||||||
|
for cached_pipeline in pipelines_to_queue {
|
||||||
|
self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;
|
||||||
|
self.waiting_pipelines.insert(cached_pipeline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_create_render_pipeline(
|
||||||
|
&mut self,
|
||||||
|
id: CachedPipelineId,
|
||||||
|
descriptor: RenderPipelineDescriptor,
|
||||||
|
) -> CachedPipelineState {
|
||||||
|
let device = self.device.clone();
|
||||||
|
let shader_cache = self.shader_cache.clone();
|
||||||
|
let layout_cache = self.layout_cache.clone();
|
||||||
|
let mut bindgroup_layout_cache = self.bindgroup_layout_cache.lock().unwrap();
|
||||||
|
let bind_group_layout = descriptor
|
||||||
|
.layout
|
||||||
|
.iter()
|
||||||
|
.map(|bind_group_layout_descriptor| {
|
||||||
|
bindgroup_layout_cache.get(&self.device, bind_group_layout_descriptor.clone())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
create_pipeline_task(
|
||||||
|
async move {
|
||||||
|
let mut shader_cache = shader_cache.lock().unwrap();
|
||||||
|
let mut layout_cache = layout_cache.lock().unwrap();
|
||||||
|
|
||||||
|
let vertex_module = match shader_cache.get(
|
||||||
|
&device,
|
||||||
|
id,
|
||||||
|
descriptor.vertex.shader.id(),
|
||||||
|
&descriptor.vertex.shader_defs,
|
||||||
|
) {
|
||||||
|
Ok(module) => module,
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
let fragment_module = match &descriptor.fragment {
|
||||||
|
Some(fragment) => {
|
||||||
|
match shader_cache.get(
|
||||||
|
&device,
|
||||||
|
id,
|
||||||
|
fragment.shader.id(),
|
||||||
|
&fragment.shader_defs,
|
||||||
|
) {
|
||||||
|
Ok(module) => Some(module),
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout =
|
||||||
|
if descriptor.layout.is_empty() && descriptor.push_constant_ranges.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(layout_cache.get(
|
||||||
|
&device,
|
||||||
|
&bind_group_layout,
|
||||||
|
descriptor.push_constant_ranges.to_vec(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
|
drop((shader_cache, layout_cache));
|
||||||
|
|
||||||
|
let vertex_buffer_layouts = descriptor
|
||||||
|
.vertex
|
||||||
|
.buffers
|
||||||
|
.iter()
|
||||||
|
.map(|layout| RawVertexBufferLayout {
|
||||||
|
array_stride: layout.array_stride,
|
||||||
|
attributes: &layout.attributes,
|
||||||
|
step_mode: layout.step_mode,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let fragment_data = descriptor.fragment.as_ref().map(|fragment| {
|
||||||
|
(
|
||||||
|
fragment_module.unwrap(),
|
||||||
|
fragment.entry_point.as_deref(),
|
||||||
|
fragment.targets.as_slice(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Expose the rest of this somehow
|
||||||
|
let compilation_options = PipelineCompilationOptions {
|
||||||
|
constants: &[],
|
||||||
|
zero_initialize_workgroup_memory: descriptor.zero_initialize_workgroup_memory,
|
||||||
|
};
|
||||||
|
|
||||||
|
let descriptor = RawRenderPipelineDescriptor {
|
||||||
|
multiview: None,
|
||||||
|
depth_stencil: descriptor.depth_stencil.clone(),
|
||||||
|
label: descriptor.label.as_deref(),
|
||||||
|
layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }),
|
||||||
|
multisample: descriptor.multisample,
|
||||||
|
primitive: descriptor.primitive,
|
||||||
|
vertex: RawVertexState {
|
||||||
|
buffers: &vertex_buffer_layouts,
|
||||||
|
entry_point: descriptor.vertex.entry_point.as_deref(),
|
||||||
|
module: &vertex_module,
|
||||||
|
// TODO: Should this be the same as the fragment compilation options?
|
||||||
|
compilation_options: compilation_options.clone(),
|
||||||
|
},
|
||||||
|
fragment: fragment_data
|
||||||
|
.as_ref()
|
||||||
|
.map(|(module, entry_point, targets)| RawFragmentState {
|
||||||
|
entry_point: entry_point.as_deref(),
|
||||||
|
module,
|
||||||
|
targets,
|
||||||
|
// TODO: Should this be the same as the vertex compilation options?
|
||||||
|
compilation_options,
|
||||||
|
}),
|
||||||
|
cache: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Pipeline::RenderPipeline(
|
||||||
|
device.create_render_pipeline(&descriptor),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
self.synchronous_pipeline_compilation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_create_compute_pipeline(
|
||||||
|
&mut self,
|
||||||
|
id: CachedPipelineId,
|
||||||
|
descriptor: ComputePipelineDescriptor,
|
||||||
|
) -> CachedPipelineState {
|
||||||
|
let device = self.device.clone();
|
||||||
|
let shader_cache = self.shader_cache.clone();
|
||||||
|
let layout_cache = self.layout_cache.clone();
|
||||||
|
let mut bindgroup_layout_cache = self.bindgroup_layout_cache.lock().unwrap();
|
||||||
|
let bind_group_layout = descriptor
|
||||||
|
.layout
|
||||||
|
.iter()
|
||||||
|
.map(|bind_group_layout_descriptor| {
|
||||||
|
bindgroup_layout_cache.get(&self.device, bind_group_layout_descriptor.clone())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
create_pipeline_task(
|
||||||
|
async move {
|
||||||
|
let mut shader_cache = shader_cache.lock().unwrap();
|
||||||
|
let mut layout_cache = layout_cache.lock().unwrap();
|
||||||
|
|
||||||
|
let compute_module = match shader_cache.get(
|
||||||
|
&device,
|
||||||
|
id,
|
||||||
|
descriptor.shader.id(),
|
||||||
|
&descriptor.shader_defs,
|
||||||
|
) {
|
||||||
|
Ok(module) => module,
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout =
|
||||||
|
if descriptor.layout.is_empty() && descriptor.push_constant_ranges.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(layout_cache.get(
|
||||||
|
&device,
|
||||||
|
&bind_group_layout,
|
||||||
|
descriptor.push_constant_ranges.to_vec(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
|
drop((shader_cache, layout_cache));
|
||||||
|
|
||||||
|
let descriptor = RawComputePipelineDescriptor {
|
||||||
|
label: descriptor.label.as_deref(),
|
||||||
|
layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }),
|
||||||
|
module: &compute_module,
|
||||||
|
entry_point: descriptor.entry_point.as_deref(),
|
||||||
|
// TODO: Expose the rest of this somehow
|
||||||
|
compilation_options: PipelineCompilationOptions {
|
||||||
|
constants: &[],
|
||||||
|
zero_initialize_workgroup_memory: descriptor
|
||||||
|
.zero_initialize_workgroup_memory,
|
||||||
|
},
|
||||||
|
cache: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Pipeline::ComputePipeline(
|
||||||
|
device.create_compute_pipeline(&descriptor),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
self.synchronous_pipeline_compilation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process the pipeline queue and create all pending pipelines if possible.
|
||||||
|
///
|
||||||
|
/// This is generally called automatically during the [`RenderSystems::Render`] step, but can
|
||||||
|
/// be called manually to force creation at a different time.
|
||||||
|
///
|
||||||
|
/// [`RenderSystems::Render`]: crate::RenderSystems::Render
|
||||||
|
pub fn process_queue(&mut self) {
|
||||||
|
let mut waiting_pipelines = mem::take(&mut self.waiting_pipelines);
|
||||||
|
let mut pipelines = mem::take(&mut self.pipelines);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut new_pipelines = self
|
||||||
|
.new_pipelines
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(PoisonError::into_inner);
|
||||||
|
for new_pipeline in new_pipelines.drain(..) {
|
||||||
|
let id = pipelines.len();
|
||||||
|
pipelines.push(new_pipeline);
|
||||||
|
waiting_pipelines.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id in waiting_pipelines {
|
||||||
|
self.process_pipeline(&mut pipelines[id], id);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pipelines = pipelines;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_pipeline(&mut self, cached_pipeline: &mut CachedPipeline, id: usize) {
|
||||||
|
match &mut cached_pipeline.state {
|
||||||
|
CachedPipelineState::Queued => {
|
||||||
|
cached_pipeline.state = match &cached_pipeline.descriptor {
|
||||||
|
PipelineDescriptor::RenderPipelineDescriptor(descriptor) => {
|
||||||
|
self.start_create_render_pipeline(id, *descriptor.clone())
|
||||||
|
}
|
||||||
|
PipelineDescriptor::ComputePipelineDescriptor(descriptor) => {
|
||||||
|
self.start_create_compute_pipeline(id, *descriptor.clone())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedPipelineState::Creating(task) => match bevy_tasks::futures::check_ready(task) {
|
||||||
|
Some(Ok(pipeline)) => {
|
||||||
|
cached_pipeline.state = CachedPipelineState::Ok(pipeline);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(Err(err)) => cached_pipeline.state = CachedPipelineState::Err(err),
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
|
||||||
|
CachedPipelineState::Err(err) => match err {
|
||||||
|
// Retry
|
||||||
|
PipelineCacheError::ShaderNotLoaded(_)
|
||||||
|
| PipelineCacheError::ShaderImportNotYetAvailable => {
|
||||||
|
cached_pipeline.state = CachedPipelineState::Queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shader could not be processed ... retrying won't help
|
||||||
|
PipelineCacheError::ProcessShaderError(err) => {
|
||||||
|
let error_detail =
|
||||||
|
err.emit_to_string(&self.shader_cache.lock().unwrap().composer);
|
||||||
|
if std::env::var("VERBOSE_SHADER_ERROR")
|
||||||
|
.is_ok_and(|v| !(v.is_empty() || v == "0" || v == "false"))
|
||||||
|
{
|
||||||
|
error!("{}", pipeline_error_context(cached_pipeline));
|
||||||
|
}
|
||||||
|
error!("failed to process shader error:\n{}", error_detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PipelineCacheError::CreateShaderModule(description) => {
|
||||||
|
error!("failed to create shader module: {}", description);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
CachedPipelineState::Ok(_) => return,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry
|
||||||
|
self.waiting_pipelines.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn process_pipeline_queue_system(mut cache: ResMut<Self>) {
|
||||||
|
cache.process_queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn extract_shaders(
|
||||||
|
mut cache: ResMut<Self>,
|
||||||
|
shaders: Extract<Res<Assets<Shader>>>,
|
||||||
|
mut events: Extract<MessageReader<AssetEvent<Shader>>>,
|
||||||
|
) {
|
||||||
|
for event in events.read() {
|
||||||
|
#[expect(
|
||||||
|
clippy::match_same_arms,
|
||||||
|
reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."
|
||||||
|
)]
|
||||||
|
match event {
|
||||||
|
// PERF: Instead of blocking waiting for the shader cache lock, try again next frame if the lock is currently held
|
||||||
|
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
|
||||||
|
if let Some(shader) = shaders.get(*id) {
|
||||||
|
let mut shader = shader.clone();
|
||||||
|
shader.shader_defs.extend(cache.global_shader_defs.clone());
|
||||||
|
|
||||||
|
cache.set_shader(*id, shader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AssetEvent::Removed { id } => cache.remove_shader(*id),
|
||||||
|
AssetEvent::Unused { .. } => {}
|
||||||
|
AssetEvent::LoadedWithDependencies { .. } => {
|
||||||
|
// TODO: handle this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pipeline_error_context(cached_pipeline: &CachedPipeline) -> String {
|
||||||
|
fn format(
|
||||||
|
shader: &Handle<Shader>,
|
||||||
|
entry: &Option<Cow<'static, str>>,
|
||||||
|
shader_defs: &[ShaderDefVal],
|
||||||
|
) -> String {
|
||||||
|
let source = match shader.path() {
|
||||||
|
Some(path) => path.path().to_string_lossy().to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
let entry = match entry {
|
||||||
|
Some(entry) => entry.to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
let shader_defs = shader_defs
|
||||||
|
.iter()
|
||||||
|
.flat_map(|def| match def {
|
||||||
|
ShaderDefVal::Bool(k, v) if *v => Some(k.to_string()),
|
||||||
|
ShaderDefVal::Int(k, v) => Some(format!("{k} = {v}")),
|
||||||
|
ShaderDefVal::UInt(k, v) => Some(format!("{k} = {v}")),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
format!("{source}:{entry}\nshader defs: {shader_defs}")
|
||||||
|
}
|
||||||
|
match &cached_pipeline.descriptor {
|
||||||
|
PipelineDescriptor::RenderPipelineDescriptor(desc) => {
|
||||||
|
let vert = &desc.vertex;
|
||||||
|
let vert_str = format(&vert.shader, &vert.entry_point, &vert.shader_defs);
|
||||||
|
let Some(frag) = desc.fragment.as_ref() else {
|
||||||
|
return vert_str;
|
||||||
|
};
|
||||||
|
let frag_str = format(&frag.shader, &frag.entry_point, &frag.shader_defs);
|
||||||
|
format!("vertex {vert_str}\nfragment {frag_str}")
|
||||||
|
}
|
||||||
|
PipelineDescriptor::ComputePipelineDescriptor(desc) => {
|
||||||
|
format(&desc.shader, &desc.entry_point, &desc.shader_defs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
not(target_arch = "wasm32"),
|
||||||
|
not(target_os = "macos"),
|
||||||
|
feature = "multi_threaded"
|
||||||
|
))]
|
||||||
|
fn create_pipeline_task(
|
||||||
|
task: impl Future<Output = Result<Pipeline, PipelineCacheError>> + Send + 'static,
|
||||||
|
sync: bool,
|
||||||
|
) -> CachedPipelineState {
|
||||||
|
if !sync {
|
||||||
|
return CachedPipelineState::Creating(bevy_tasks::AsyncComputeTaskPool::get().spawn(task));
|
||||||
|
}
|
||||||
|
|
||||||
|
match bevy_tasks::block_on(task) {
|
||||||
|
Ok(pipeline) => CachedPipelineState::Ok(pipeline),
|
||||||
|
Err(err) => CachedPipelineState::Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
target_arch = "wasm32",
|
||||||
|
target_os = "macos",
|
||||||
|
not(feature = "multi_threaded")
|
||||||
|
))]
|
||||||
|
fn create_pipeline_task(
|
||||||
|
task: impl Future<Output = Result<Pipeline, PipelineCacheError>> + Send + 'static,
|
||||||
|
_sync: bool,
|
||||||
|
) -> CachedPipelineState {
|
||||||
|
match bevy_tasks::block_on(task) {
|
||||||
|
Ok(pipeline) => CachedPipelineState::Ok(pipeline),
|
||||||
|
Err(err) => CachedPipelineState::Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
259
third_party/bevy_render/src/render_resource/pipeline_specializer.rs
vendored
Normal file
259
third_party/bevy_render/src/render_resource/pipeline_specializer.rs
vendored
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
use crate::render_resource::{
|
||||||
|
CachedComputePipelineId, CachedRenderPipelineId, ComputePipelineDescriptor, PipelineCache,
|
||||||
|
RenderPipelineDescriptor,
|
||||||
|
};
|
||||||
|
use bevy_ecs::resource::Resource;
|
||||||
|
use bevy_mesh::{MeshVertexBufferLayoutRef, MissingVertexAttributeError, VertexBufferLayout};
|
||||||
|
use bevy_platform::{
|
||||||
|
collections::{
|
||||||
|
hash_map::{Entry, RawEntryMut, VacantEntry},
|
||||||
|
HashMap,
|
||||||
|
},
|
||||||
|
hash::FixedHasher,
|
||||||
|
};
|
||||||
|
use bevy_utils::default;
|
||||||
|
use core::{fmt::Debug, hash::Hash};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
/// A trait that allows constructing different variants of a render pipeline from a key.
|
||||||
|
///
|
||||||
|
/// Note: This is intended for modifying your pipeline descriptor on the basis of a key. If your key
|
||||||
|
/// contains no data then you don't need to specialize. For example, if you are using the
|
||||||
|
/// [`AsBindGroup`](crate::render_resource::AsBindGroup) without the `#[bind_group_data]` attribute,
|
||||||
|
/// you don't need to specialize. Instead, create the pipeline directly from [`PipelineCache`] and
|
||||||
|
/// store its ID.
|
||||||
|
///
|
||||||
|
/// See [`SpecializedRenderPipelines`] for more info.
|
||||||
|
pub trait SpecializedRenderPipeline {
|
||||||
|
/// The key that defines each "variant" of the render pipeline.
|
||||||
|
type Key: Clone + Hash + PartialEq + Eq;
|
||||||
|
|
||||||
|
/// Construct a new render pipeline based on the provided key.
|
||||||
|
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience cache for creating different variants of a render pipeline based on some key.
|
||||||
|
///
|
||||||
|
/// Some render pipelines may need to be configured differently depending on the exact situation.
|
||||||
|
/// This cache allows constructing different render pipelines for each situation based on a key,
|
||||||
|
/// making it easy to A) construct the necessary pipelines, and B) reuse already constructed
|
||||||
|
/// pipelines.
|
||||||
|
///
|
||||||
|
/// Note: This is intended for modifying your pipeline descriptor on the basis of a key. If your key
|
||||||
|
/// contains no data then you don't need to specialize. For example, if you are using the
|
||||||
|
/// [`AsBindGroup`](crate::render_resource::AsBindGroup) without the `#[bind_group_data]` attribute,
|
||||||
|
/// you don't need to specialize. Instead, create the pipeline directly from [`PipelineCache`] and
|
||||||
|
/// store its ID.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct SpecializedRenderPipelines<S: SpecializedRenderPipeline> {
|
||||||
|
cache: HashMap<S::Key, CachedRenderPipelineId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SpecializedRenderPipeline> Default for SpecializedRenderPipelines<S> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { cache: default() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SpecializedRenderPipeline> SpecializedRenderPipelines<S> {
|
||||||
|
/// Get or create a specialized instance of the pipeline corresponding to `key`.
|
||||||
|
pub fn specialize(
|
||||||
|
&mut self,
|
||||||
|
cache: &PipelineCache,
|
||||||
|
pipeline_specializer: &S,
|
||||||
|
key: S::Key,
|
||||||
|
) -> CachedRenderPipelineId {
|
||||||
|
*self.cache.entry(key.clone()).or_insert_with(|| {
|
||||||
|
let descriptor = pipeline_specializer.specialize(key);
|
||||||
|
cache.queue_render_pipeline(descriptor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait that allows constructing different variants of a compute pipeline from a key.
|
||||||
|
///
|
||||||
|
/// Note: This is intended for modifying your pipeline descriptor on the basis of a key. If your key
|
||||||
|
/// contains no data then you don't need to specialize. For example, if you are using the
|
||||||
|
/// [`AsBindGroup`](crate::render_resource::AsBindGroup) without the `#[bind_group_data]` attribute,
|
||||||
|
/// you don't need to specialize. Instead, create the pipeline directly from [`PipelineCache`] and
|
||||||
|
/// store its ID.
|
||||||
|
///
|
||||||
|
/// See [`SpecializedComputePipelines`] for more info.
|
||||||
|
pub trait SpecializedComputePipeline {
|
||||||
|
/// The key that defines each "variant" of the compute pipeline.
|
||||||
|
type Key: Clone + Hash + PartialEq + Eq;
|
||||||
|
|
||||||
|
/// Construct a new compute pipeline based on the provided key.
|
||||||
|
fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience cache for creating different variants of a compute pipeline based on some key.
|
||||||
|
///
|
||||||
|
/// Some compute pipelines may need to be configured differently depending on the exact situation.
|
||||||
|
/// This cache allows constructing different compute pipelines for each situation based on a key,
|
||||||
|
/// making it easy to A) construct the necessary pipelines, and B) reuse already constructed
|
||||||
|
/// pipelines.
|
||||||
|
///
|
||||||
|
/// Note: This is intended for modifying your pipeline descriptor on the basis of a key. If your key
|
||||||
|
/// contains no data then you don't need to specialize. For example, if you are using the
|
||||||
|
/// [`AsBindGroup`](crate::render_resource::AsBindGroup) without the `#[bind_group_data]` attribute,
|
||||||
|
/// you don't need to specialize. Instead, create the pipeline directly from [`PipelineCache`] and
|
||||||
|
/// store its ID.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct SpecializedComputePipelines<S: SpecializedComputePipeline> {
|
||||||
|
cache: HashMap<S::Key, CachedComputePipelineId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SpecializedComputePipeline> Default for SpecializedComputePipelines<S> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { cache: default() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SpecializedComputePipeline> SpecializedComputePipelines<S> {
|
||||||
|
/// Get or create a specialized instance of the pipeline corresponding to `key`.
|
||||||
|
pub fn specialize(
|
||||||
|
&mut self,
|
||||||
|
cache: &PipelineCache,
|
||||||
|
specialize_pipeline: &S,
|
||||||
|
key: S::Key,
|
||||||
|
) -> CachedComputePipelineId {
|
||||||
|
*self.cache.entry(key.clone()).or_insert_with(|| {
|
||||||
|
let descriptor = specialize_pipeline.specialize(key);
|
||||||
|
cache.queue_compute_pipeline(descriptor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait that allows constructing different variants of a render pipeline from a key and the
|
||||||
|
/// particular mesh's vertex buffer layout.
|
||||||
|
///
|
||||||
|
/// See [`SpecializedMeshPipelines`] for more info.
|
||||||
|
pub trait SpecializedMeshPipeline {
|
||||||
|
/// The key that defines each "variant" of the render pipeline.
|
||||||
|
type Key: Clone + Hash + PartialEq + Eq;
|
||||||
|
|
||||||
|
/// Construct a new render pipeline based on the provided key and vertex layout.
|
||||||
|
///
|
||||||
|
/// The returned pipeline descriptor should have a single vertex buffer, which is derived from
|
||||||
|
/// `layout`.
|
||||||
|
fn specialize(
|
||||||
|
&self,
|
||||||
|
key: Self::Key,
|
||||||
|
layout: &MeshVertexBufferLayoutRef,
|
||||||
|
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cache of different variants of a render pipeline based on a key and the particular mesh's
|
||||||
|
/// vertex buffer layout.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct SpecializedMeshPipelines<S: SpecializedMeshPipeline> {
|
||||||
|
mesh_layout_cache: HashMap<(MeshVertexBufferLayoutRef, S::Key), CachedRenderPipelineId>,
|
||||||
|
vertex_layout_cache: VertexLayoutCache<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type VertexLayoutCache<S> = HashMap<
|
||||||
|
VertexBufferLayout,
|
||||||
|
HashMap<<S as SpecializedMeshPipeline>::Key, CachedRenderPipelineId>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
impl<S: SpecializedMeshPipeline> Default for SpecializedMeshPipelines<S> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mesh_layout_cache: Default::default(),
|
||||||
|
vertex_layout_cache: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SpecializedMeshPipeline> SpecializedMeshPipelines<S> {
|
||||||
|
/// Construct a new render pipeline based on the provided key and the mesh's vertex buffer
|
||||||
|
/// layout.
|
||||||
|
#[inline]
|
||||||
|
pub fn specialize(
|
||||||
|
&mut self,
|
||||||
|
cache: &PipelineCache,
|
||||||
|
pipeline_specializer: &S,
|
||||||
|
key: S::Key,
|
||||||
|
layout: &MeshVertexBufferLayoutRef,
|
||||||
|
) -> Result<CachedRenderPipelineId, SpecializedMeshPipelineError> {
|
||||||
|
return match self.mesh_layout_cache.entry((layout.clone(), key.clone())) {
|
||||||
|
Entry::Occupied(entry) => Ok(*entry.into_mut()),
|
||||||
|
Entry::Vacant(entry) => specialize_slow(
|
||||||
|
&mut self.vertex_layout_cache,
|
||||||
|
cache,
|
||||||
|
pipeline_specializer,
|
||||||
|
key,
|
||||||
|
layout,
|
||||||
|
entry,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cold]
|
||||||
|
fn specialize_slow<S>(
|
||||||
|
vertex_layout_cache: &mut VertexLayoutCache<S>,
|
||||||
|
cache: &PipelineCache,
|
||||||
|
specialize_pipeline: &S,
|
||||||
|
key: S::Key,
|
||||||
|
layout: &MeshVertexBufferLayoutRef,
|
||||||
|
entry: VacantEntry<
|
||||||
|
(MeshVertexBufferLayoutRef, S::Key),
|
||||||
|
CachedRenderPipelineId,
|
||||||
|
FixedHasher,
|
||||||
|
>,
|
||||||
|
) -> Result<CachedRenderPipelineId, SpecializedMeshPipelineError>
|
||||||
|
where
|
||||||
|
S: SpecializedMeshPipeline,
|
||||||
|
{
|
||||||
|
let descriptor = specialize_pipeline
|
||||||
|
.specialize(key.clone(), layout)
|
||||||
|
.map_err(|mut err| {
|
||||||
|
{
|
||||||
|
let SpecializedMeshPipelineError::MissingVertexAttribute(err) = &mut err;
|
||||||
|
err.pipeline_type = Some(core::any::type_name::<S>());
|
||||||
|
}
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
// Different MeshVertexBufferLayouts can produce the same final VertexBufferLayout
|
||||||
|
// We want compatible vertex buffer layouts to use the same pipelines, so we must "deduplicate" them
|
||||||
|
let layout_map = match vertex_layout_cache
|
||||||
|
.raw_entry_mut()
|
||||||
|
.from_key(&descriptor.vertex.buffers[0])
|
||||||
|
{
|
||||||
|
RawEntryMut::Occupied(entry) => entry.into_mut(),
|
||||||
|
RawEntryMut::Vacant(entry) => {
|
||||||
|
entry
|
||||||
|
.insert(descriptor.vertex.buffers[0].clone(), Default::default())
|
||||||
|
.1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(*entry.insert(match layout_map.entry(key) {
|
||||||
|
Entry::Occupied(entry) => {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
let stored_descriptor = cache.get_render_pipeline_descriptor(*entry.get());
|
||||||
|
if stored_descriptor != &descriptor {
|
||||||
|
error!(
|
||||||
|
"The cached pipeline descriptor for {} is not \
|
||||||
|
equal to the generated descriptor for the given key. \
|
||||||
|
This means the SpecializePipeline implementation uses \
|
||||||
|
unused' MeshVertexBufferLayout information to specialize \
|
||||||
|
the pipeline. This is not allowed because it would invalidate \
|
||||||
|
the pipeline cache.",
|
||||||
|
core::any::type_name::<S>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*entry.into_mut()
|
||||||
|
}
|
||||||
|
Entry::Vacant(entry) => *entry.insert(cache.queue_render_pipeline(descriptor)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum SpecializedMeshPipelineError {
|
||||||
|
#[error(transparent)]
|
||||||
|
MissingVertexAttribute(#[from] MissingVertexAttributeError),
|
||||||
|
}
|
||||||
39
third_party/bevy_render/src/render_resource/resource_macros.rs
vendored
Normal file
39
third_party/bevy_render/src/render_resource/resource_macros.rs
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! define_atomic_id {
|
||||||
|
($atomic_id_type:ident) => {
|
||||||
|
#[derive(Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)]
|
||||||
|
pub struct $atomic_id_type(core::num::NonZero<u32>);
|
||||||
|
|
||||||
|
impl $atomic_id_type {
|
||||||
|
#[expect(
|
||||||
|
clippy::new_without_default,
|
||||||
|
reason = "Implementing the `Default` trait on atomic IDs would imply that two `<AtomicIdType>::default()` equal each other. By only implementing `new()`, we indicate that each atomic ID created will be unique."
|
||||||
|
)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
use core::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
|
static COUNTER: AtomicU32 = AtomicU32::new(1);
|
||||||
|
|
||||||
|
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||||
|
Self(core::num::NonZero::<u32>::new(counter).unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"The system ran out of unique `{}`s.",
|
||||||
|
stringify!($atomic_id_type)
|
||||||
|
);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$atomic_id_type> for core::num::NonZero<u32> {
|
||||||
|
fn from(value: $atomic_id_type) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<core::num::NonZero<u32>> for $atomic_id_type {
|
||||||
|
fn from(value: core::num::NonZero<u32>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
353
third_party/bevy_render/src/render_resource/specializer.rs
vendored
Normal file
353
third_party/bevy_render/src/render_resource/specializer.rs
vendored
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
use super::{
|
||||||
|
CachedComputePipelineId, CachedRenderPipelineId, ComputePipeline, ComputePipelineDescriptor,
|
||||||
|
PipelineCache, RenderPipeline, RenderPipelineDescriptor,
|
||||||
|
};
|
||||||
|
use bevy_ecs::error::BevyError;
|
||||||
|
use bevy_platform::{
|
||||||
|
collections::{
|
||||||
|
hash_map::{Entry, VacantEntry},
|
||||||
|
HashMap,
|
||||||
|
},
|
||||||
|
hash::FixedHasher,
|
||||||
|
};
|
||||||
|
use core::{hash::Hash, marker::PhantomData};
|
||||||
|
use tracing::error;
|
||||||
|
use variadics_please::all_tuples;
|
||||||
|
|
||||||
|
pub use bevy_render_macros::{Specializer, SpecializerKey};
|
||||||
|
|
||||||
|
/// Defines a type that is able to be "specialized" and cached by creating and transforming
|
||||||
|
/// its descriptor type. This is implemented for [`RenderPipeline`] and [`ComputePipeline`], and
|
||||||
|
/// likely will not have much utility for other types.
|
||||||
|
///
|
||||||
|
/// See docs on [`Specializer`] for more info.
|
||||||
|
pub trait Specializable {
|
||||||
|
type Descriptor: PartialEq + Clone + Send + Sync;
|
||||||
|
type CachedId: Clone + Send + Sync;
|
||||||
|
fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId;
|
||||||
|
fn get_descriptor(pipeline_cache: &PipelineCache, id: Self::CachedId) -> &Self::Descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Specializable for RenderPipeline {
|
||||||
|
type Descriptor = RenderPipelineDescriptor;
|
||||||
|
type CachedId = CachedRenderPipelineId;
|
||||||
|
|
||||||
|
fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId {
|
||||||
|
pipeline_cache.queue_render_pipeline(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_descriptor(
|
||||||
|
pipeline_cache: &PipelineCache,
|
||||||
|
id: CachedRenderPipelineId,
|
||||||
|
) -> &Self::Descriptor {
|
||||||
|
pipeline_cache.get_render_pipeline_descriptor(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Specializable for ComputePipeline {
|
||||||
|
type Descriptor = ComputePipelineDescriptor;
|
||||||
|
|
||||||
|
type CachedId = CachedComputePipelineId;
|
||||||
|
|
||||||
|
fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId {
|
||||||
|
pipeline_cache.queue_compute_pipeline(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_descriptor(
|
||||||
|
pipeline_cache: &PipelineCache,
|
||||||
|
id: CachedComputePipelineId,
|
||||||
|
) -> &Self::Descriptor {
|
||||||
|
pipeline_cache.get_compute_pipeline_descriptor(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defines a type capable of "specializing" values of a type T.
|
||||||
|
///
|
||||||
|
/// Specialization is the process of generating variants of a type T
|
||||||
|
/// from small hashable keys, and specializers themselves can be
|
||||||
|
/// thought of as [pure functions] from the key type to `T`, that
|
||||||
|
/// [memoize] their results based on the key.
|
||||||
|
///
|
||||||
|
/// <div class="warning">
|
||||||
|
/// Because specialization is designed for use with render and compute
|
||||||
|
/// pipelines, specializers act on <i>descriptors</i> of <code>T</code> rather
|
||||||
|
/// than produce <code>T</code> itself, but the above comparison is still valid.
|
||||||
|
/// </div>
|
||||||
|
///
|
||||||
|
/// Since compiling render and compute pipelines can be so slow,
|
||||||
|
/// specialization allows a Bevy app to detect when it would compile
|
||||||
|
/// a duplicate pipeline and reuse what's already in the cache. While
|
||||||
|
/// pipelines could all be memoized hashing each whole descriptor, this
|
||||||
|
/// would be much slower and could still create duplicates. In contrast,
|
||||||
|
/// memoizing groups of *related* pipelines based on a small hashable
|
||||||
|
/// key is much faster. See the docs on [`SpecializerKey`] for more info.
|
||||||
|
///
|
||||||
|
/// ## Composing Specializers
|
||||||
|
///
|
||||||
|
/// This trait can be derived with `#[derive(Specializer)]` for structs whose
|
||||||
|
/// fields all implement [`Specializer`]. This allows for composing multiple
|
||||||
|
/// specializers together, and makes encapsulation and separating concerns
|
||||||
|
/// between specializers much nicer. One could make individual specializers
|
||||||
|
/// for common operations and place them in entirely separate modules, then
|
||||||
|
/// compose them together with a single `#[derive]`
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use bevy_ecs::error::BevyError;
|
||||||
|
/// # use bevy_render::render_resource::Specializer;
|
||||||
|
/// # use bevy_render::render_resource::SpecializerKey;
|
||||||
|
/// # use bevy_render::render_resource::RenderPipeline;
|
||||||
|
/// # use bevy_render::render_resource::RenderPipelineDescriptor;
|
||||||
|
/// struct A;
|
||||||
|
/// struct B;
|
||||||
|
/// #[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)]
|
||||||
|
/// struct BKey { contrived_number: u32 };
|
||||||
|
///
|
||||||
|
/// impl Specializer<RenderPipeline> for A {
|
||||||
|
/// type Key = ();
|
||||||
|
///
|
||||||
|
/// fn specialize(
|
||||||
|
/// &self,
|
||||||
|
/// key: (),
|
||||||
|
/// descriptor: &mut RenderPipelineDescriptor
|
||||||
|
/// ) -> Result<(), BevyError> {
|
||||||
|
/// # let _ = descriptor;
|
||||||
|
/// // mutate the descriptor here
|
||||||
|
/// Ok(key)
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Specializer<RenderPipeline> for B {
|
||||||
|
/// type Key = BKey;
|
||||||
|
///
|
||||||
|
/// fn specialize(
|
||||||
|
/// &self,
|
||||||
|
/// key: BKey,
|
||||||
|
/// descriptor: &mut RenderPipelineDescriptor
|
||||||
|
/// ) -> Result<BKey, BevyError> {
|
||||||
|
/// # let _ = descriptor;
|
||||||
|
/// // mutate the descriptor here
|
||||||
|
/// Ok(key)
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(Specializer)]
|
||||||
|
/// #[specialize(RenderPipeline)]
|
||||||
|
/// struct C {
|
||||||
|
/// #[key(default)]
|
||||||
|
/// a: A,
|
||||||
|
/// b: B,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// /*
|
||||||
|
/// The generated implementation:
|
||||||
|
/// impl Specializer<RenderPipeline> for C {
|
||||||
|
/// type Key = BKey;
|
||||||
|
/// fn specialize(
|
||||||
|
/// &self,
|
||||||
|
/// key: Self::Key,
|
||||||
|
/// descriptor: &mut RenderPipelineDescriptor
|
||||||
|
/// ) -> Result<Canonical<Self::Key>, BevyError> {
|
||||||
|
/// let _ = self.a.specialize((), descriptor);
|
||||||
|
/// let key = self.b.specialize(key, descriptor);
|
||||||
|
/// Ok(key)
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// */
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The key type for a composed specializer will be a tuple of the keys
|
||||||
|
/// of each field, and their specialization logic will be applied in field
|
||||||
|
/// order. Since derive macros can't have generic parameters, the derive macro
|
||||||
|
/// requires an additional `#[specialize(..targets)]` attribute to specify a
|
||||||
|
/// list of types to target for the implementation. `#[specialize(all)]` is
|
||||||
|
/// also allowed, and will generate a fully generic implementation at the cost
|
||||||
|
/// of slightly worse error messages.
|
||||||
|
///
|
||||||
|
/// Additionally, each field can optionally take a `#[key]` attribute to
|
||||||
|
/// specify a "key override". This will hide that field's key from being
|
||||||
|
/// exposed by the wrapper, and always use the value given by the attribute.
|
||||||
|
/// Values for this attribute may either be `default` which will use the key's
|
||||||
|
/// [`Default`] implementation, or a valid rust expression of the key type.
|
||||||
|
///
|
||||||
|
/// [pure functions]: https://en.wikipedia.org/wiki/Pure_function
|
||||||
|
/// [memoize]: https://en.wikipedia.org/wiki/Memoization
|
||||||
|
pub trait Specializer<T: Specializable>: Send + Sync + 'static {
|
||||||
|
type Key: SpecializerKey;
|
||||||
|
fn specialize(
|
||||||
|
&self,
|
||||||
|
key: Self::Key,
|
||||||
|
descriptor: &mut T::Descriptor,
|
||||||
|
) -> Result<Canonical<Self::Key>, BevyError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: update docs for `SpecializerKey` with a more concrete example
|
||||||
|
// once we've migrated mesh layout specialization
|
||||||
|
|
||||||
|
/// Defines a type that is able to be used as a key for [`Specializer`]s
|
||||||
|
///
|
||||||
|
/// <div class = "warning">
|
||||||
|
/// <strong>Most types should implement this trait with the included derive macro.</strong> <br/>
|
||||||
|
/// This generates a "canonical" key type, with <code>IS_CANONICAL = true</code>, and <code>Canonical = Self</code>
|
||||||
|
/// </div>
|
||||||
|
///
|
||||||
|
/// ## What's a "canonical" key?
|
||||||
|
///
|
||||||
|
/// The specialization API memoizes pipelines based on the hash of each key, but this
|
||||||
|
/// can still produce duplicates. For example, if one used a list of vertex attributes
|
||||||
|
/// as a key, even if all the same attributes were present they could be in any order.
|
||||||
|
/// In each case, though the keys would be "different" they would produce the same
|
||||||
|
/// pipeline.
|
||||||
|
///
|
||||||
|
/// To address this, during specialization keys are processed into a [canonical]
|
||||||
|
/// (or "standard") form that represents the actual descriptor that was produced.
|
||||||
|
/// In the previous example, that would be the final `VertexBufferLayout` contained
|
||||||
|
/// by the pipeline descriptor. This new key is used by [`Variants`] to
|
||||||
|
/// perform additional checks for duplicates, but only if required. If a key is
|
||||||
|
/// canonical from the start, then there's no need.
|
||||||
|
///
|
||||||
|
/// For implementors: the main property of a canonical key is that if two keys hash
|
||||||
|
/// differently, they should nearly always produce different descriptors.
|
||||||
|
///
|
||||||
|
/// [canonical]: https://en.wikipedia.org/wiki/Canonicalization
|
||||||
|
pub trait SpecializerKey: Clone + Hash + Eq {
|
||||||
|
/// Denotes whether this key is canonical or not. This should only be `true`
|
||||||
|
/// if and only if `Canonical = Self`.
|
||||||
|
const IS_CANONICAL: bool;
|
||||||
|
|
||||||
|
/// The canonical key type to convert this into during specialization.
|
||||||
|
type Canonical: Hash + Eq;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Canonical<T> = <T as SpecializerKey>::Canonical;
|
||||||
|
|
||||||
|
impl<T: Specializable> Specializer<T> for () {
|
||||||
|
type Key = ();
|
||||||
|
|
||||||
|
fn specialize(
|
||||||
|
&self,
|
||||||
|
_key: Self::Key,
|
||||||
|
_descriptor: &mut T::Descriptor,
|
||||||
|
) -> Result<(), BevyError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Specializable, V: Send + Sync + 'static> Specializer<T> for PhantomData<V> {
|
||||||
|
type Key = ();
|
||||||
|
|
||||||
|
fn specialize(
|
||||||
|
&self,
|
||||||
|
_key: Self::Key,
|
||||||
|
_descriptor: &mut T::Descriptor,
|
||||||
|
) -> Result<(), BevyError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_specialization_key_tuple {
|
||||||
|
($(#[$meta:meta])* $($T:ident),*) => {
|
||||||
|
$(#[$meta])*
|
||||||
|
impl <$($T: SpecializerKey),*> SpecializerKey for ($($T,)*) {
|
||||||
|
const IS_CANONICAL: bool = true $(&& <$T as SpecializerKey>::IS_CANONICAL)*;
|
||||||
|
type Canonical = ($(Canonical<$T>,)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
all_tuples!(
|
||||||
|
#[doc(fake_variadic)]
|
||||||
|
impl_specialization_key_tuple,
|
||||||
|
0,
|
||||||
|
12,
|
||||||
|
T
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A cache for variants of a resource type created by a specializer.
|
||||||
|
/// At most one resource will be created for each key.
|
||||||
|
pub struct Variants<T: Specializable, S: Specializer<T>> {
|
||||||
|
specializer: S,
|
||||||
|
base_descriptor: T::Descriptor,
|
||||||
|
primary_cache: HashMap<S::Key, T::CachedId>,
|
||||||
|
secondary_cache: HashMap<Canonical<S::Key>, T::CachedId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Specializable, S: Specializer<T>> Variants<T, S> {
|
||||||
|
/// Creates a new [`Variants`] from a [`Specializer`] and a base descriptor.
|
||||||
|
#[inline]
|
||||||
|
pub fn new(specializer: S, base_descriptor: T::Descriptor) -> Self {
|
||||||
|
Self {
|
||||||
|
specializer,
|
||||||
|
base_descriptor,
|
||||||
|
primary_cache: Default::default(),
|
||||||
|
secondary_cache: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specializes a resource given the [`Specializer`]'s key type.
|
||||||
|
#[inline]
|
||||||
|
pub fn specialize(
|
||||||
|
&mut self,
|
||||||
|
pipeline_cache: &PipelineCache,
|
||||||
|
key: S::Key,
|
||||||
|
) -> Result<T::CachedId, BevyError> {
|
||||||
|
let entry = self.primary_cache.entry(key.clone());
|
||||||
|
match entry {
|
||||||
|
Entry::Occupied(entry) => Ok(entry.get().clone()),
|
||||||
|
Entry::Vacant(entry) => Self::specialize_slow(
|
||||||
|
&self.specializer,
|
||||||
|
self.base_descriptor.clone(),
|
||||||
|
pipeline_cache,
|
||||||
|
key,
|
||||||
|
entry,
|
||||||
|
&mut self.secondary_cache,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
|
fn specialize_slow(
|
||||||
|
specializer: &S,
|
||||||
|
base_descriptor: T::Descriptor,
|
||||||
|
pipeline_cache: &PipelineCache,
|
||||||
|
key: S::Key,
|
||||||
|
primary_entry: VacantEntry<S::Key, T::CachedId, FixedHasher>,
|
||||||
|
secondary_cache: &mut HashMap<Canonical<S::Key>, T::CachedId>,
|
||||||
|
) -> Result<T::CachedId, BevyError> {
|
||||||
|
let mut descriptor = base_descriptor.clone();
|
||||||
|
let canonical_key = specializer.specialize(key.clone(), &mut descriptor)?;
|
||||||
|
|
||||||
|
// if the whole key is canonical, the secondary cache isn't needed.
|
||||||
|
if <S::Key as SpecializerKey>::IS_CANONICAL {
|
||||||
|
return Ok(primary_entry
|
||||||
|
.insert(<T as Specializable>::queue(pipeline_cache, descriptor))
|
||||||
|
.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = match secondary_cache.entry(canonical_key) {
|
||||||
|
Entry::Occupied(entry) => {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
let stored_descriptor =
|
||||||
|
<T as Specializable>::get_descriptor(pipeline_cache, entry.get().clone());
|
||||||
|
if &descriptor != stored_descriptor {
|
||||||
|
error!(
|
||||||
|
"Invalid Specializer<{}> impl for {}: the cached descriptor \
|
||||||
|
is not equal to the generated descriptor for the given key. \
|
||||||
|
This means the Specializer implementation uses unused information \
|
||||||
|
from the key to specialize the pipeline. This is not allowed \
|
||||||
|
because it would invalidate the cache.",
|
||||||
|
core::any::type_name::<T>(),
|
||||||
|
core::any::type_name::<S>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.into_mut().clone()
|
||||||
|
}
|
||||||
|
Entry::Vacant(entry) => entry
|
||||||
|
.insert(<T as Specializable>::queue(pipeline_cache, descriptor))
|
||||||
|
.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
primary_entry.insert(id.clone());
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
285
third_party/bevy_render/src/render_resource/storage_buffer.rs
vendored
Normal file
285
third_party/bevy_render/src/render_resource/storage_buffer.rs
vendored
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
use super::Buffer;
|
||||||
|
use crate::renderer::{RenderDevice, RenderQueue};
|
||||||
|
use encase::{
|
||||||
|
internal::WriteInto, DynamicStorageBuffer as DynamicStorageBufferWrapper, ShaderType,
|
||||||
|
StorageBuffer as StorageBufferWrapper,
|
||||||
|
};
|
||||||
|
use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferSize, BufferUsages};
|
||||||
|
|
||||||
|
use super::IntoBinding;
|
||||||
|
|
||||||
|
/// Stores data to be transferred to the GPU and made accessible to shaders as a storage buffer.
|
||||||
|
///
|
||||||
|
/// Storage buffers can be made available to shaders in some combination of read/write mode, and can store large amounts of data.
|
||||||
|
/// Note however that WebGL2 does not support storage buffers, so consider alternative options in this case.
|
||||||
|
///
|
||||||
|
/// Storage buffers can store runtime-sized arrays, but only if they are the last field in a structure.
|
||||||
|
///
|
||||||
|
/// The contained data is stored in system RAM. [`write_buffer`](StorageBuffer::write_buffer) queues
|
||||||
|
/// copying of the data from system RAM to VRAM. Storage buffers must conform to [std430 alignment/padding requirements], which
|
||||||
|
/// is automatically enforced by this structure.
|
||||||
|
///
|
||||||
|
/// Other options for storing GPU-accessible data are:
|
||||||
|
/// * [`BufferVec`](crate::render_resource::BufferVec)
|
||||||
|
/// * [`DynamicStorageBuffer`]
|
||||||
|
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
|
||||||
|
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
|
||||||
|
/// * [`RawBufferVec`](crate::render_resource::RawBufferVec)
|
||||||
|
/// * [`Texture`](crate::render_resource::Texture)
|
||||||
|
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
|
||||||
|
///
|
||||||
|
/// [std430 alignment/padding requirements]: https://www.w3.org/TR/WGSL/#address-spaces-storage
|
||||||
|
pub struct StorageBuffer<T: ShaderType> {
|
||||||
|
value: T,
|
||||||
|
scratch: StorageBufferWrapper<Vec<u8>>,
|
||||||
|
buffer: Option<Buffer>,
|
||||||
|
label: Option<String>,
|
||||||
|
changed: bool,
|
||||||
|
buffer_usage: BufferUsages,
|
||||||
|
last_written_size: Option<BufferSize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ShaderType> From<T> for StorageBuffer<T> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
value,
|
||||||
|
scratch: StorageBufferWrapper::new(Vec::new()),
|
||||||
|
buffer: None,
|
||||||
|
label: None,
|
||||||
|
changed: false,
|
||||||
|
buffer_usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
|
||||||
|
last_written_size: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ShaderType + Default> Default for StorageBuffer<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
value: T::default(),
|
||||||
|
scratch: StorageBufferWrapper::new(Vec::new()),
|
||||||
|
buffer: None,
|
||||||
|
label: None,
|
||||||
|
changed: false,
|
||||||
|
buffer_usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
|
||||||
|
last_written_size: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ShaderType + WriteInto> StorageBuffer<T> {
|
||||||
|
#[inline]
|
||||||
|
pub fn buffer(&self) -> Option<&Buffer> {
|
||||||
|
self.buffer.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn binding(&self) -> Option<BindingResource<'_>> {
|
||||||
|
Some(BindingResource::Buffer(BufferBinding {
|
||||||
|
buffer: self.buffer()?,
|
||||||
|
offset: 0,
|
||||||
|
size: self.last_written_size,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, value: T) {
|
||||||
|
self.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> &T {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self) -> &mut T {
|
||||||
|
&mut self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_label(&mut self, label: Option<&str>) {
|
||||||
|
let label = label.map(str::to_string);
|
||||||
|
|
||||||
|
if label != self.label {
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_label(&self) -> Option<&str> {
|
||||||
|
self.label.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add more [`BufferUsages`] to the buffer.
|
||||||
|
///
|
||||||
|
/// This method only allows addition of flags to the default usage flags.
|
||||||
|
///
|
||||||
|
/// The default values for buffer usage are `BufferUsages::COPY_DST` and `BufferUsages::STORAGE`.
|
||||||
|
pub fn add_usages(&mut self, usage: BufferUsages) {
|
||||||
|
self.buffer_usage |= usage;
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queues writing of data from system RAM to VRAM using the [`RenderDevice`]
|
||||||
|
/// and the provided [`RenderQueue`].
|
||||||
|
///
|
||||||
|
/// If there is no GPU-side buffer allocated to hold the data currently stored, or if a GPU-side buffer previously
|
||||||
|
/// allocated does not have enough capacity, a new GPU-side buffer is created.
|
||||||
|
pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) {
|
||||||
|
self.scratch.write(&self.value).unwrap();
|
||||||
|
|
||||||
|
let capacity = self.buffer.as_deref().map(wgpu::Buffer::size).unwrap_or(0);
|
||||||
|
let size = self.scratch.as_ref().len() as u64;
|
||||||
|
|
||||||
|
if capacity < size || self.changed {
|
||||||
|
self.buffer = Some(device.create_buffer_with_data(&BufferInitDescriptor {
|
||||||
|
label: self.label.as_deref(),
|
||||||
|
usage: self.buffer_usage,
|
||||||
|
contents: self.scratch.as_ref(),
|
||||||
|
}));
|
||||||
|
self.changed = false;
|
||||||
|
} else if let Some(buffer) = &self.buffer {
|
||||||
|
queue.write_buffer(buffer, 0, self.scratch.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_written_size = BufferSize::new(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a StorageBuffer<T> {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
self.binding().expect("Failed to get buffer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores data to be transferred to the GPU and made accessible to shaders as a dynamic storage buffer.
|
||||||
|
///
|
||||||
|
/// This is just a [`StorageBuffer`], but also allows you to set dynamic offsets.
|
||||||
|
///
|
||||||
|
/// Dynamic storage buffers can be made available to shaders in some combination of read/write mode, and can store large amounts
|
||||||
|
/// of data. Note however that WebGL2 does not support storage buffers, so consider alternative options in this case. Dynamic
|
||||||
|
/// storage buffers support multiple separate bindings at dynamic byte offsets and so have a
|
||||||
|
/// [`push`](DynamicStorageBuffer::push) method.
|
||||||
|
///
|
||||||
|
/// The contained data is stored in system RAM. [`write_buffer`](DynamicStorageBuffer::write_buffer)
|
||||||
|
/// queues copying of the data from system RAM to VRAM. The data within a storage buffer binding must conform to
|
||||||
|
/// [std430 alignment/padding requirements]. `DynamicStorageBuffer` takes care of serializing the inner type to conform to
|
||||||
|
/// these requirements. Each item [`push`](DynamicStorageBuffer::push)ed into this structure
|
||||||
|
/// will additionally be aligned to meet dynamic offset alignment requirements.
|
||||||
|
///
|
||||||
|
/// Other options for storing GPU-accessible data are:
|
||||||
|
/// * [`BufferVec`](crate::render_resource::BufferVec)
|
||||||
|
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
|
||||||
|
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
|
||||||
|
/// * [`RawBufferVec`](crate::render_resource::RawBufferVec)
|
||||||
|
/// * [`StorageBuffer`]
|
||||||
|
/// * [`Texture`](crate::render_resource::Texture)
|
||||||
|
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
|
||||||
|
///
|
||||||
|
/// [std430 alignment/padding requirements]: https://www.w3.org/TR/WGSL/#address-spaces-storage
|
||||||
|
pub struct DynamicStorageBuffer<T: ShaderType> {
|
||||||
|
scratch: DynamicStorageBufferWrapper<Vec<u8>>,
|
||||||
|
buffer: Option<Buffer>,
|
||||||
|
label: Option<String>,
|
||||||
|
changed: bool,
|
||||||
|
buffer_usage: BufferUsages,
|
||||||
|
last_written_size: Option<BufferSize>,
|
||||||
|
_marker: PhantomData<fn() -> T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ShaderType> Default for DynamicStorageBuffer<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
scratch: DynamicStorageBufferWrapper::new(Vec::new()),
|
||||||
|
buffer: None,
|
||||||
|
label: None,
|
||||||
|
changed: false,
|
||||||
|
buffer_usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
|
||||||
|
last_written_size: None,
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ShaderType + WriteInto> DynamicStorageBuffer<T> {
|
||||||
|
#[inline]
|
||||||
|
pub fn buffer(&self) -> Option<&Buffer> {
|
||||||
|
self.buffer.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn binding(&self) -> Option<BindingResource<'_>> {
|
||||||
|
Some(BindingResource::Buffer(BufferBinding {
|
||||||
|
buffer: self.buffer()?,
|
||||||
|
offset: 0,
|
||||||
|
size: self.last_written_size,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.scratch.as_ref().is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn push(&mut self, value: T) -> u32 {
|
||||||
|
self.scratch.write(&value).unwrap() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_label(&mut self, label: Option<&str>) {
|
||||||
|
let label = label.map(str::to_string);
|
||||||
|
|
||||||
|
if label != self.label {
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_label(&self) -> Option<&str> {
|
||||||
|
self.label.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add more [`BufferUsages`] to the buffer.
|
||||||
|
///
|
||||||
|
/// This method only allows addition of flags to the default usage flags.
|
||||||
|
///
|
||||||
|
/// The default values for buffer usage are `BufferUsages::COPY_DST` and `BufferUsages::STORAGE`.
|
||||||
|
pub fn add_usages(&mut self, usage: BufferUsages) {
|
||||||
|
self.buffer_usage |= usage;
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) {
|
||||||
|
let capacity = self.buffer.as_deref().map(wgpu::Buffer::size).unwrap_or(0);
|
||||||
|
let size = self.scratch.as_ref().len() as u64;
|
||||||
|
|
||||||
|
if capacity < size || (self.changed && size > 0) {
|
||||||
|
self.buffer = Some(device.create_buffer_with_data(&BufferInitDescriptor {
|
||||||
|
label: self.label.as_deref(),
|
||||||
|
usage: self.buffer_usage,
|
||||||
|
contents: self.scratch.as_ref(),
|
||||||
|
}));
|
||||||
|
self.changed = false;
|
||||||
|
} else if let Some(buffer) = &self.buffer {
|
||||||
|
queue.write_buffer(buffer, 0, self.scratch.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_written_size = BufferSize::new(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.scratch.as_mut().clear();
|
||||||
|
self.scratch.set_offset(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a DynamicStorageBuffer<T> {
|
||||||
|
#[inline]
|
||||||
|
fn into_binding(self) -> BindingResource<'a> {
|
||||||
|
self.binding().expect("Failed to get buffer")
|
||||||
|
}
|
||||||
|
}
|
||||||
166
third_party/bevy_render/src/render_resource/texture.rs
vendored
Normal file
166
third_party/bevy_render/src/render_resource/texture.rs
vendored
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
use crate::define_atomic_id;
|
||||||
|
use crate::renderer::WgpuWrapper;
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::resource::Resource;
|
||||||
|
use core::ops::Deref;
|
||||||
|
|
||||||
|
define_atomic_id!(TextureId);
|
||||||
|
|
||||||
|
/// A GPU-accessible texture.
|
||||||
|
///
|
||||||
|
/// May be converted from and dereferences to a wgpu [`Texture`](wgpu::Texture).
|
||||||
|
/// Can be created via [`RenderDevice::create_texture`](crate::renderer::RenderDevice::create_texture).
|
||||||
|
///
|
||||||
|
/// Other options for storing GPU-accessible data are:
|
||||||
|
/// * [`BufferVec`](crate::render_resource::BufferVec)
|
||||||
|
/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer)
|
||||||
|
/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer)
|
||||||
|
/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer)
|
||||||
|
/// * [`RawBufferVec`](crate::render_resource::RawBufferVec)
|
||||||
|
/// * [`StorageBuffer`](crate::render_resource::StorageBuffer)
|
||||||
|
/// * [`UniformBuffer`](crate::render_resource::UniformBuffer)
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Texture {
|
||||||
|
id: TextureId,
|
||||||
|
value: WgpuWrapper<wgpu::Texture>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Texture {
|
||||||
|
/// Returns the [`TextureId`].
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> TextureId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a view of this texture.
|
||||||
|
pub fn create_view(&self, desc: &wgpu::TextureViewDescriptor) -> TextureView {
|
||||||
|
TextureView::from(self.value.create_view(desc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wgpu::Texture> for Texture {
|
||||||
|
fn from(value: wgpu::Texture) -> Self {
|
||||||
|
Texture {
|
||||||
|
id: TextureId::new(),
|
||||||
|
value: WgpuWrapper::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Texture {
|
||||||
|
type Target = wgpu::Texture;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
define_atomic_id!(TextureViewId);
|
||||||
|
|
||||||
|
/// Describes a [`Texture`] with its associated metadata required by a pipeline or [`BindGroup`](super::BindGroup).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TextureView {
|
||||||
|
id: TextureViewId,
|
||||||
|
value: WgpuWrapper<wgpu::TextureView>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SurfaceTexture {
|
||||||
|
value: WgpuWrapper<wgpu::SurfaceTexture>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SurfaceTexture {
|
||||||
|
pub fn present(self) {
|
||||||
|
self.value.into_inner().present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextureView {
|
||||||
|
/// Returns the [`TextureViewId`].
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> TextureViewId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wgpu::TextureView> for TextureView {
|
||||||
|
fn from(value: wgpu::TextureView) -> Self {
|
||||||
|
TextureView {
|
||||||
|
id: TextureViewId::new(),
|
||||||
|
value: WgpuWrapper::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wgpu::SurfaceTexture> for SurfaceTexture {
|
||||||
|
fn from(value: wgpu::SurfaceTexture) -> Self {
|
||||||
|
SurfaceTexture {
|
||||||
|
value: WgpuWrapper::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for TextureView {
|
||||||
|
type Target = wgpu::TextureView;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SurfaceTexture {
|
||||||
|
type Target = wgpu::SurfaceTexture;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
define_atomic_id!(SamplerId);
|
||||||
|
|
||||||
|
/// A Sampler defines how a pipeline will sample from a [`TextureView`].
|
||||||
|
/// They define image filters (including anisotropy) and address (wrapping) modes, among other things.
|
||||||
|
///
|
||||||
|
/// May be converted from and dereferences to a wgpu [`Sampler`](wgpu::Sampler).
|
||||||
|
/// Can be created via [`RenderDevice::create_sampler`](crate::renderer::RenderDevice::create_sampler).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Sampler {
|
||||||
|
id: SamplerId,
|
||||||
|
value: WgpuWrapper<wgpu::Sampler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sampler {
|
||||||
|
/// Returns the [`SamplerId`].
|
||||||
|
#[inline]
|
||||||
|
pub fn id(&self) -> SamplerId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wgpu::Sampler> for Sampler {
|
||||||
|
fn from(value: wgpu::Sampler) -> Self {
|
||||||
|
Sampler {
|
||||||
|
id: SamplerId::new(),
|
||||||
|
value: WgpuWrapper::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Sampler {
|
||||||
|
type Target = wgpu::Sampler;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rendering resource for the default image sampler which is set during renderer
|
||||||
|
/// initialization.
|
||||||
|
///
|
||||||
|
/// The [`ImagePlugin`](bevy_image::ImagePlugin) can be set during app initialization to change the default
|
||||||
|
/// image sampler.
|
||||||
|
#[derive(Resource, Debug, Clone, Deref, DerefMut)]
|
||||||
|
pub struct DefaultImageSampler(pub(crate) Sampler);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user