diff --git a/Cargo.lock b/Cargo.lock index da4aa33d..6b42a08a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,9 +254,6 @@ name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -dependencies = [ - "serde", -] [[package]] name = "block" @@ -889,6 +886,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.5.0" @@ -1044,34 +1047,17 @@ dependencies = [ "gl_generator", ] -[[package]] -name = "gpu-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" -dependencies = [ - "bitflags 2.9.0", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" -dependencies = [ - "bitflags 2.9.0", -] - [[package]] name = "gpu-allocator" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" dependencies = [ + "ash", + "hashbrown 0.16.1", "log", "presser", - "thiserror 1.0.69", + "thiserror 2.0.16", "windows", ] @@ -1149,7 +1135,18 @@ checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -1368,6 +1365,7 @@ dependencies = [ "rand", "shaderc", "wgpu", + "windows", "winit", ] @@ -1404,7 +1402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if 1.0.0", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -1556,9 +1554,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.32.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" dependencies = [ "bitflags 2.9.0", "block", @@ -1614,9 +1612,9 @@ dependencies = [ [[package]] name = "naga" -version = "26.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916cbc7cb27db60be930a4e2da243cf4bc39569195f22fd8ee419cd31d5b662c" +checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135" dependencies = [ "arrayvec", "bit-set", @@ -1625,7 +1623,7 @@ dependencies = [ "cfg_aliases 0.2.1", "codespan-reporting", "half", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "hexf-parse", "indexmap 2.9.0", "libm", @@ -3149,16 +3147,17 @@ dependencies = [ [[package]] name = "wgpu" -version = "26.0.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70b6ff82bbf6e9206828e1a3178e851f8c20f1c9028e74dd3a8090741ccd5798" +checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f" dependencies = [ "arrayvec", "bitflags 2.9.0", + "bytemuck", "cfg-if 1.0.0", "cfg_aliases 0.2.1", "document-features", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "js-sys", "log", "naga", @@ -3178,9 +3177,9 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "26.0.1" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f62f1053bd28c2268f42916f31588f81f64796e2ff91b81293515017ca8bd9" +checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9" dependencies = [ "arrayvec", "bit-set", @@ -3189,7 +3188,7 @@ dependencies = [ "bytemuck", "cfg_aliases 0.2.1", "document-features", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "indexmap 2.9.0", "log", "naga", @@ -3211,45 +3210,45 @@ dependencies = [ [[package]] name = "wgpu-core-deps-apple" -version = "26.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18ae5fbde6a4cbebae38358aa73fcd6e0f15c6144b67ef5dc91ded0db125dbdf" +checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-emscripten" -version = "26.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7670e390f416006f746b4600fdd9136455e3627f5bd763abf9a65daa216dd2d" +checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-wasm" -version = "26.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03b9f9e1a50686d315fc6debe4980cc45cd37b0e919351917df494e8fdc8885" +checksum = "12a2cf578ce8d7d50d0e63ddc2345c7dcb599f6eb90b888813406ea78b9b7010" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "26.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720a5cb9d12b3d337c15ff0e24d3e97ed11490ff3f7506e7f3d98c68fa5d6f14" +checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "26.0.4" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df2c64ac282a91ad7662c90bc4a77d4a2135bc0b2a2da5a4d4e267afc034b9e" +checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef" dependencies = [ "android_system_properties", "arrayvec", @@ -3263,10 +3262,9 @@ dependencies = [ "core-graphics-types 0.2.0", "glow", "glutin_wgl_sys", - "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "js-sys", "khronos-egl", "libc", @@ -3276,6 +3274,7 @@ dependencies = [ "naga", "ndk-sys 0.6.0+11769913", "objc", + "once_cell", "ordered-float", "parking_lot", "portable-atomic", @@ -3295,15 +3294,14 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "26.0.0" +version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" +checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c" dependencies = [ "bitflags 2.9.0", "bytemuck", "js-sys", "log", - "thiserror 2.0.16", "web-sys", ] @@ -3340,32 +3338,54 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.58.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", - "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.58.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", + "windows-link", "windows-result", "windows-strings", - "windows-targets 0.52.6", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -3374,9 +3394,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -3385,27 +3405,36 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] [[package]] name = "windows-result" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -3490,6 +3519,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 4776ace2..83aea4b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,17 +3,17 @@ resolver = "2" # Sub packages provided by lambda. members = [ - "crates/lambda-rs", - "crates/lambda-rs-args", - "crates/lambda-rs-logging", - "crates/lambda-rs-platform", - "tools/obj_loader" + "crates/lambda-rs", + "crates/lambda-rs-args", + "crates/lambda-rs-logging", + "crates/lambda-rs-platform", + "tools/obj_loader", ] default-members = [ - "crates/lambda-rs", - "crates/lambda-rs-args", - "crates/lambda-rs-logging", - "crates/lambda-rs-platform", - "tools/obj_loader" + "crates/lambda-rs", + "crates/lambda-rs-args", + "crates/lambda-rs-logging", + "crates/lambda-rs-platform", + "tools/obj_loader", ] diff --git a/README.md b/README.md index bdf7b824..c2160553 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ ![lambda-rs](https://img.shields.io/crates/v/lambda-rs) ## Table of contents + 1. [Description](#description) 1. [API Documentation](#documentation) 1. [Building](#building) @@ -23,13 +24,16 @@ 1. [Releases & Publishing](#publishing) 1. [How to contribute](#contribute) 1. [Resources](#resources) + ## Description + Lambda is a framework for developing cross platform applications and workloads using Rust. Lambda aims to enable developers to create highly performant, portable, and minimal desktop applications by providing a platform agnostic API for all of the features that any application or workload might need. Lambda : + * Desktop applications * Productivity tools * Data visualizations @@ -49,11 +53,13 @@ lightweight HTML/CSS based rendering in the future but we're primarily focused on implementing a Rust native UI framework built on top of our rendering engine. ## Documentation + * [lambda-rs API documentation](https://docs.rs/lambda-rs/2023.1.29/lambda/) ## Installation ### From crates.io + lambda is now available on [crates.io](https://crates.io/crates/lambda-rs) and can be added to your project by adding the following to your `Cargo.toml` file: @@ -64,6 +70,7 @@ lambda-rs = "2023.1.30" ``` or from the CLI: + ```bash cargo add lambda-rs ``` @@ -71,135 +78,170 @@ cargo add lambda-rs ### From source #### Required external dependencies + * All platforms - * `cmake >= 3.20.0` is needed to build shaderc from source. - * `ninja` is needed to build shaderc from source. - * `git` is needed to clone the project and manage it's dependencies. - * `git-lfs` is needed for asset files. - * `rust >= 1.60` is needed for compiling lambda and all of it's crates. - * `pre-commit` is used for development git commit hooks and any changes that do not pass the pre-commit checks will not be accepted. + * `cmake >= 3.20.0` is needed to build shaderc from source. + * `ninja` is needed to build shaderc from source. + * `git` is needed to clone the project and manage it's dependencies. + * `git-lfs` is needed for asset files. + * `rust >= 1.60` is needed for compiling lambda and all of it's crates. + * `pre-commit` is used for development git commit hooks and any changes that do not pass the pre-commit checks will not be accepted. #### Rendering API support + These are the Rendering APIs that are supported on each platform and must be installed manually. More information on how to choose which backend lambda uses on each platform is provided further below. + * Windows - * `OpenGL` - * `Vulkan` - * `DirectX11` - * `DirectX12` + * `OpenGL` + * `Vulkan` + * `DirectX11` + * `DirectX12` * Linux - * `OpenGL` - * `Vulkan` + * `OpenGL` + * `Vulkan` * MacOS - * `Metal` - * `Vulkan` + * `Metal` + * `Vulkan` #### Linux (bash), MacOS (bash), Windows (git-bash) + If planning to develop for lambda, you must run the setup script provided by repository like so: + ```bash ./scripts/setup.sh ``` + This will initialize pre commit checks for development use and setup git-lfs for asset management. In order to validate that lambda successfully compiles, you can build the library by performing a build with cargo. + ```bash cargo build --lib ``` + If this works successfully, then lambda is ready to work on your system! ## Getting started + Coming soon. ## Tutorials + Start with the tutorials to build features step by step: -- Tutorials index: [docs/tutorials/](./docs/tutorials/) -- Uniform Buffers: Build a Spinning Triangle: [docs/tutorials/uniform-buffers.md](./docs/tutorials/uniform-buffers.md) +* Tutorials index: [docs/tutorials/](./docs/tutorials/) +* Uniform Buffers: Build a Spinning Triangle: [docs/tutorials/uniform-buffers.md](./docs/tutorials/uniform-buffers.md) ## Examples + Browse example sources: -- Core API examples: [crates/lambda-rs/examples/](./crates/lambda-rs/examples/) -- Logging examples: [crates/lambda-rs-logging/examples/](./crates/lambda-rs-logging/examples/) -- Argument parsing examples: [crates/lambda-rs-args/examples/](./crates/lambda-rs-args/examples/) +* Core API examples: [crates/lambda-rs/examples/](./crates/lambda-rs/examples/) +* Logging examples: [crates/lambda-rs-logging/examples/](./crates/lambda-rs-logging/examples/) +* Argument parsing examples: [crates/lambda-rs-args/examples/](./crates/lambda-rs-args/examples/) ### Minimal + A minimal example of an application with a working window using lambda. + ```rust cargo run --example minimal ``` -### Push Constants -An example of using shaders with push constants to render a 3D image. +### Immediates + +An example of using shaders with immediates (per-draw data) to render a 3D image. + ```rust -cargo run --example push_constants +cargo run --example immediates ``` #### Notes + * On windows, you need to run this example with `--features lambda-rs/with-vulkan` as the shader used in the example does not work in either dx11 or dx12. ### Triangle + An example using shaders to render a single triangle. + ```rust cargo run --example triangle ``` ### Triangles + An example using shaders to render multiple triangles and keyboard input to move one of the triangles on screen. + ```rust cargo run --example triangles ``` ## Plans -- ### Architecture support - - [x] x86 - - [x] arm64 -- ### Operating system support - - [x] MacOS - - [x] Linux - - [x] Windows 10/11 - - [ ] Xbox Series S|X (Long term goal) - - [ ] iOS (Long term goal) - - [ ] Android (Long term goal) -- ### Rendering API support - - [x] OpenGL - - [x] Vulkan - - [x] Metal - - [x] DirectX11 - - [x] DirectX12 -- ### Packages - - [x] (WIP) [lambda-rs-args](./crates/lambda-rs-args/README.md) -- Command line argument parsing. - - [x] (WIP) [lambda-rs-platform](./crates/lambda-rs-platform/README.md) -- Dependency wrappers & platform support. - - [x] [lambda-rs-logging](./crates/lambda-rs-logging/README.md) -- Lightweight Logging API for lambda-rs packages. - - [x] (WIP) [lambda-rs](./crates/lambda-rs/README.md) -- The public Lambda API. -- ### Examples - - [x] Minimal -- A minimal example of an application with a working window + +* ### Architecture support + + * [x] x86 + * [x] arm64 + +* ### Operating system support + + * [x] MacOS + * [x] Linux + * [x] Windows 10/11 + * [ ] Xbox Series S|X (Long term goal) + * [ ] iOS (Long term goal) + * [ ] Android (Long term goal) + +* ### Rendering API support + + * [x] OpenGL + * [x] Vulkan + * [x] Metal + * [x] DirectX11 + * [x] DirectX12 + +* ### Packages + + * [x] (WIP) [lambda-rs-args](./crates/lambda-rs-args/README.md) -- Command line argument parsing. + * [x] (WIP) [lambda-rs-platform](./crates/lambda-rs-platform/README.md) -- Dependency wrappers & platform support. + * [x] [lambda-rs-logging](./crates/lambda-rs-logging/README.md) -- Lightweight Logging API for lambda-rs packages. + * [x] (WIP) [lambda-rs](./crates/lambda-rs/README.md) -- The public Lambda API. + +* ### Examples + + * [x] Minimal -- A minimal example of an application with a working window using lambda. - - [x] Push Constants -- An example of using shaders with push constants to + * [x] Immediates -- An example of using shaders with immediate data to render a 3D image. - - [x] Triangle -- An example using shaders to render a single triangle. - - [x] Triangles -- An example using shaders to render multiple triangles and keyboard input to move one of the triangles on screen. -- ### Tools - - [x] obj-loader -- (WIP) Loads .obj files into lambda. Meshes need to be triangulated in order for it to render at the moment. - - [ ] platform-info -- Utility for viewing information about the current platform. -- ### CI/CD - - [x] Github action pipelines for building lambda on all platforms. - - [ ] Github action pipelines for creating downloadable builds from releases. - - [ ] Tests & benchmarking. - - [ ] Unit tests. - - [ ] Nightly builds. + * [x] Triangle -- An example using shaders to render a single triangle. + * [x] Triangles -- An example using shaders to render multiple triangles and keyboard input to move one of the triangles on screen. + +* ### Tools + + * [x] obj-loader -- (WIP) Loads .obj files into lambda. Meshes need to be triangulated in order for it to render at the moment. + * [ ] platform-info -- Utility for viewing information about the current platform. + +* ### CI/CD + + * [x] Github action pipelines for building lambda on all platforms. + * [ ] Github action pipelines for creating downloadable builds from releases. + * [ ] Tests & benchmarking. + * [ ] Unit tests. + * [ ] Nightly builds. ## Releases & Publishing + For cutting releases, publishing crates to crates.io, and attaching multi-platform artifacts to GitHub Releases, see: -- docs/publishing.md - +* docs/publishing.md ## How to contribute + Fork the current repository and then make the changes that you'd like to said fork. Stable releases will happen within the main branch requiring that additions to be made off of `dev` which is the nightly build branch for lambda. @@ -211,6 +253,7 @@ changes made, my schedule, and any other variable factors. They must also pass all of the build pipelines are configured to run at merge. ## Resources + [The Cherno's playlist for making a game engine](https://www.youtube.com/playlist?list=PLlrATfBNZ98dC-V-N3m0Go4deliWHPFwT) [Creator of Logo](https://github.com/RinniSwift) diff --git a/crates/lambda-rs-platform/Cargo.toml b/crates/lambda-rs-platform/Cargo.toml index 3eb1b17a..62fc2155 100644 --- a/crates/lambda-rs-platform/Cargo.toml +++ b/crates/lambda-rs-platform/Cargo.toml @@ -13,28 +13,47 @@ path = "src/lib.rs" [dependencies] winit = "=0.29.15" shaderc = { version = "=0.7", optional = true, default-features = false } -naga = { version = "=26.0.0", optional = true, default-features = false, features = ["spv-out", "glsl-in", "wgsl-in"] } +naga = { version = "=28.0.0", optional = true, default-features = false, features = [ + "spv-out", + "glsl-in", + "wgsl-in", +] } rand = "=0.8.5" obj-rs = "=0.7.0" -wgpu = { version = "=26.0.1", optional = true, features = ["wgsl", "spirv"] } +wgpu = { version = "=28.0.0", optional = true, features = ["wgsl", "spirv"] } pollster = { version = "=0.4.0", optional = true } lambda-rs-logging = { path = "../lambda-rs-logging", version = "2023.1.30" } +# Force windows crate to 0.62 to unify wgpu-hal and gpu-allocator dependencies. +# Both crates support this version range, but Cargo may resolve to different +# versions without this explicit constraint. +[target.'cfg(windows)'.dependencies] +windows = "0.62" + [dev-dependencies] mockall = "=0.11.3" [features] -default=["wgpu", "shader-backend-naga"] +default = ["wgpu", "shader-backend-naga"] -shader-backend-naga=["dep:naga"] -shader-backend-shaderc=["dep:shaderc"] -shader-backend-shaderc-build-from-source=["shader-backend-shaderc", "shaderc/build-from-source"] +shader-backend-naga = ["dep:naga"] +shader-backend-shaderc = ["dep:shaderc"] +shader-backend-shaderc-build-from-source = [ + "shader-backend-shaderc", + "shaderc/build-from-source", +] -wgpu=["dep:wgpu", "dep:pollster", "wgpu/wgsl", "wgpu/spirv", "shader-backend-naga"] -wgpu-with-vulkan=["wgpu"] -wgpu-with-metal=["wgpu", "wgpu/metal"] -wgpu-with-dx12=["wgpu", "wgpu/dx12"] -wgpu-with-gl=["wgpu", "wgpu/webgl"] +wgpu = [ + "dep:wgpu", + "dep:pollster", + "wgpu/wgsl", + "wgpu/spirv", + "shader-backend-naga", +] +wgpu-with-vulkan = ["wgpu"] +wgpu-with-metal = ["wgpu", "wgpu/metal"] +wgpu-with-dx12 = ["wgpu", "wgpu/dx12"] +wgpu-with-gl = ["wgpu", "wgpu/webgl"] [profile.dev] crate-type = ["cdylib", "rlib"] diff --git a/crates/lambda-rs-platform/src/wgpu/gpu.rs b/crates/lambda-rs-platform/src/wgpu/gpu.rs index 81da708e..23cc6732 100644 --- a/crates/lambda-rs-platform/src/wgpu/gpu.rs +++ b/crates/lambda-rs-platform/src/wgpu/gpu.rs @@ -46,9 +46,6 @@ impl MemoryHints { pub struct Features(wgpu::Features); impl Features { - /// Enable push constants support. - pub const PUSH_CONSTANTS: Features = Features(wgpu::Features::PUSH_CONSTANTS); - pub(crate) fn to_wgpu(self) -> wgpu::Features { self.0 } @@ -82,13 +79,13 @@ pub struct GpuBuilder { } impl GpuBuilder { - /// Create a builder with defaults favoring performance and push constants. + /// Create a builder with defaults favoring performance. pub fn new() -> Self { Self { label: Some("Lambda GPU".to_string()), power_preference: PowerPreference::HighPerformance, force_fallback_adapter: false, - required_features: Features::PUSH_CONSTANTS, + required_features: Features(wgpu::Features::IMMEDIATES), memory_hints: MemoryHints::Performance, } } @@ -152,6 +149,7 @@ impl GpuBuilder { label: self.label.as_deref(), required_features: self.required_features.to_wgpu(), required_limits: adapter.limits(), + experimental_features: wgpu::ExperimentalFeatures::default(), memory_hints: self.memory_hints.to_wgpu(), trace: wgpu::Trace::Off, }; diff --git a/crates/lambda-rs-platform/src/wgpu/pipeline.rs b/crates/lambda-rs-platform/src/wgpu/pipeline.rs index 50b61dbf..dc3eb4c3 100644 --- a/crates/lambda-rs-platform/src/wgpu/pipeline.rs +++ b/crates/lambda-rs-platform/src/wgpu/pipeline.rs @@ -15,7 +15,7 @@ use crate::wgpu::{ vertex::ColorFormat, }; -/// Shader stage flags for push constants and visibility. +/// Shader stage flags for visibility. #[derive(Clone, Copy, Debug)] /// /// This wrapper avoids exposing `wgpu` directly to higher layers while still @@ -51,10 +51,9 @@ impl std::ops::BitOrAssign for PipelineStage { } } -/// Push constant declaration for a stage and byte range. +/// Immediate data declaration for a byte range. #[derive(Clone, Debug)] -pub struct PushConstantRange { - pub stages: PipelineStage, +pub struct ImmediateDataRange { pub range: Range, } @@ -227,16 +226,100 @@ impl PipelineLayout { pub struct PipelineLayoutBuilder<'a> { label: Option, layouts: Vec<&'a bind::BindGroupLayout>, - push_constant_ranges: Vec, + immediate_data_ranges: Vec, +} + +/// Align a `u32` value up to the provided power-of-two alignment. +fn align_up_u32(value: u32, alignment: u32) -> u32 { + if alignment == 0 { + return value; + } + let remainder = value % alignment; + if remainder == 0 { + return value; + } + return value + (alignment - remainder); +} + +/// Validate immediate ranges and calculate the minimum allocation size. +/// +/// wgpu v28 uses a single byte region of size `immediate_size`, addressed by +/// `set_immediates(offset, data)`. This function enforces that the provided +/// ranges: +/// - Start at byte offset 0 (as a union) +/// - Cover a contiguous span with no gaps (as a union) +/// - Are aligned to `wgpu::IMMEDIATE_DATA_ALIGNMENT` +fn validate_and_calculate_immediate_size( + immediate_data_ranges: &[ImmediateDataRange], +) -> Result { + if immediate_data_ranges.is_empty() { + return Ok(0); + } + + let alignment = wgpu::IMMEDIATE_DATA_ALIGNMENT; + + let mut sorted_ranges: Vec> = immediate_data_ranges + .iter() + .map(|r| r.range.clone()) + .collect(); + sorted_ranges.sort_by_key(|range| (range.start, range.end)); + + for range in &sorted_ranges { + if range.start > range.end { + return Err(format!( + "Immediate data range start {} exceeds end {}.", + range.start, range.end + )); + } + if range.start == range.end { + return Err(format!( + "Immediate data range {}..{} is empty.", + range.start, range.end + )); + } + if range.start % alignment != 0 { + return Err(format!( + "Immediate data range start {} is not aligned to {} bytes.", + range.start, alignment + )); + } + if range.end % alignment != 0 { + return Err(format!( + "Immediate data range end {} is not aligned to {} bytes.", + range.end, alignment + )); + } + } + + let mut current_end = 0; + for range in &sorted_ranges { + if range.start > current_end { + return Err(format!( + "Immediate data ranges must be contiguous starting at 0; found gap \ +{}..{}.", + current_end, range.start + )); + } + current_end = current_end.max(range.end); + } + + if current_end % alignment != 0 { + return Err(format!( + "Immediate data size {} is not aligned to {} bytes.", + current_end, alignment + )); + } + + return Ok(current_end); } impl<'a> PipelineLayoutBuilder<'a> { - /// New builder with no layouts or push constants. + /// New builder with no layouts or immediate data. pub fn new() -> Self { return Self { label: None, layouts: Vec::new(), - push_constant_ranges: Vec::new(), + immediate_data_ranges: Vec::new(), }; } @@ -252,9 +335,12 @@ impl<'a> PipelineLayoutBuilder<'a> { return self; } - /// Provide push constant ranges. - pub fn with_push_constants(mut self, ranges: Vec) -> Self { - self.push_constant_ranges = ranges; + /// Provide immediate data byte ranges. + pub fn with_immediate_data_ranges( + mut self, + ranges: Vec, + ) -> Self { + self.immediate_data_ranges = ranges; return self; } @@ -262,14 +348,32 @@ impl<'a> PipelineLayoutBuilder<'a> { pub fn build(self, gpu: &Gpu) -> PipelineLayout { let layouts_raw: Vec<&wgpu::BindGroupLayout> = self.layouts.iter().map(|l| l.raw()).collect(); - let push_constants_raw: Vec = self - .push_constant_ranges - .iter() - .map(|pcr| wgpu::PushConstantRange { - stages: pcr.stages.to_wgpu(), - range: pcr.range.clone(), - }) - .collect(); + + // wgpu v28 allocates a single immediate byte region sized by + // `PipelineLayoutDescriptor::immediate_size`. If callers provide multiple + // ranges, they are treated as sub-ranges of the same contiguous allocation. + // + // Validate that the union of ranges starts at 0 and has no gaps so that the + // required allocation size is well-defined. + let (immediate_size, fallback_used) = + match validate_and_calculate_immediate_size(&self.immediate_data_ranges) { + Ok(size) => (size, false), + Err(message) => { + logging::error!( + "Invalid immediate data ranges for pipeline layout: {}", + message + ); + debug_assert!(false, "{}", message); + + let max_end = self + .immediate_data_ranges + .iter() + .map(|r| r.range.end) + .max() + .unwrap_or(0); + (align_up_u32(max_end, wgpu::IMMEDIATE_DATA_ALIGNMENT), true) + } + }; let raw = gpu @@ -277,8 +381,14 @@ impl<'a> PipelineLayoutBuilder<'a> { .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: self.label.as_deref(), bind_group_layouts: &layouts_raw, - push_constant_ranges: &push_constants_raw, + immediate_size, }); + if fallback_used { + logging::warn!( + "Pipeline layout immediate size computed using fallback; consider \ +declaring immediate ranges as a single contiguous span starting at 0." + ); + } return PipelineLayout { raw, label: self.label, @@ -286,6 +396,57 @@ impl<'a> PipelineLayoutBuilder<'a> { } } +#[cfg(test)] +mod immediate_size_tests { + use super::{ + validate_and_calculate_immediate_size, + ImmediateDataRange, + }; + + #[test] + fn immediate_size_empty_ok() { + let size = validate_and_calculate_immediate_size(&[]).unwrap(); + assert_eq!(size, 0); + } + + #[test] + fn immediate_size_overlapping_ranges_ok() { + let ranges = vec![ + ImmediateDataRange { range: 0..64 }, + ImmediateDataRange { range: 0..32 }, + ]; + let size = validate_and_calculate_immediate_size(&ranges).unwrap(); + assert_eq!(size, 64); + } + + #[test] + fn immediate_size_contiguous_ranges_ok() { + let ranges = vec![ + ImmediateDataRange { range: 0..16 }, + ImmediateDataRange { range: 16..32 }, + ]; + let size = validate_and_calculate_immediate_size(&ranges).unwrap(); + assert_eq!(size, 32); + } + + #[test] + fn immediate_size_gap_is_error() { + let ranges = vec![ + ImmediateDataRange { range: 0..16 }, + ImmediateDataRange { range: 32..48 }, + ]; + let err = validate_and_calculate_immediate_size(&ranges).unwrap_err(); + assert!(err.contains("gap")); + } + + #[test] + fn immediate_size_non_zero_start_is_error() { + let ranges = vec![ImmediateDataRange { range: 16..32 }]; + let err = validate_and_calculate_immediate_size(&ranges).unwrap_err(); + assert!(err.contains("gap")); + } +} + /// Wrapper around `wgpu::RenderPipeline`. #[derive(Debug)] pub struct RenderPipeline { @@ -533,7 +694,7 @@ impl<'a> RenderPipelineBuilder<'a> { ..wgpu::MultisampleState::default() }, fragment, - multiview: None, + multiview_mask: None, cache: None, }); diff --git a/crates/lambda-rs-platform/src/wgpu/render_pass.rs b/crates/lambda-rs-platform/src/wgpu/render_pass.rs index b6b9946e..8f2d2d38 100644 --- a/crates/lambda-rs-platform/src/wgpu/render_pass.rs +++ b/crates/lambda-rs-platform/src/wgpu/render_pass.rs @@ -173,16 +173,6 @@ impl<'a> RenderPass<'a> { .set_index_buffer(buffer.raw().slice(..), format.to_wgpu()); } - /// Upload push constants. - pub fn set_push_constants( - &mut self, - stages: pipeline::PipelineStage, - offset: u32, - data: &[u8], - ) { - self.raw.set_push_constants(stages.to_wgpu(), offset, data); - } - /// Issue a non-indexed draw over a vertex range. pub fn draw( &mut self, @@ -433,6 +423,7 @@ impl RenderPassBuilder { depth_stencil_attachment, timestamp_writes: None, occlusion_query_set: None, + multiview_mask: None, }; let pass = encoder.begin_render_pass_raw(&desc); @@ -445,4 +436,12 @@ impl<'a> RenderPass<'a> { pub fn set_stencil_reference(&mut self, reference: u32) { self.raw.set_stencil_reference(reference); } + + /// Set immediate data for subsequent draw calls. + /// + /// This is the wgpu v28 replacement for push constants. The `offset` and + /// `data` length MUST be multiples of 4 bytes (IMMEDIATE_DATA_ALIGNMENT). + pub fn set_immediates(&mut self, offset: u32, data: &[u8]) { + self.raw.set_immediates(offset, data); + } } diff --git a/crates/lambda-rs-platform/src/wgpu/surface.rs b/crates/lambda-rs-platform/src/wgpu/surface.rs index f2b9308e..eab21668 100644 --- a/crates/lambda-rs-platform/src/wgpu/surface.rs +++ b/crates/lambda-rs-platform/src/wgpu/surface.rs @@ -53,7 +53,6 @@ impl PresentMode { wgpu::PresentMode::Mailbox => PresentMode::Mailbox, wgpu::PresentMode::AutoVsync => PresentMode::AutoVsync, wgpu::PresentMode::AutoNoVsync => PresentMode::AutoNoVsync, - _ => PresentMode::Fifo, }; } } diff --git a/crates/lambda-rs-platform/src/wgpu/texture.rs b/crates/lambda-rs-platform/src/wgpu/texture.rs index ec3fe653..de554375 100644 --- a/crates/lambda-rs-platform/src/wgpu/texture.rs +++ b/crates/lambda-rs-platform/src/wgpu/texture.rs @@ -98,6 +98,13 @@ impl FilterMode { FilterMode::Linear => wgpu::FilterMode::Linear, }; } + + pub(crate) fn to_wgpu_mipmap(self) -> wgpu::MipmapFilterMode { + return match self { + FilterMode::Nearest => wgpu::MipmapFilterMode::Nearest, + FilterMode::Linear => wgpu::MipmapFilterMode::Linear, + }; + } } /// Texture addressing mode when sampling outside the [0,1] range. @@ -600,7 +607,7 @@ impl SamplerBuilder { address_mode_w: self.address_w.to_wgpu(), mag_filter: self.mag_filter.to_wgpu(), min_filter: self.min_filter.to_wgpu(), - mipmap_filter: self.mipmap_filter.to_wgpu(), + mipmap_filter: self.mipmap_filter.to_wgpu_mipmap(), lod_min_clamp: self.lod_min, lod_max_clamp: self.lod_max, ..Default::default() @@ -1028,7 +1035,7 @@ mod tests { assert_eq!(d.address_mode_w, wgpu::AddressMode::ClampToEdge); assert_eq!(d.mag_filter, wgpu::FilterMode::Nearest); assert_eq!(d.min_filter, wgpu::FilterMode::Nearest); - assert_eq!(d.mipmap_filter, wgpu::FilterMode::Nearest); + assert_eq!(d.mipmap_filter, wgpu::MipmapFilterMode::Nearest); assert_eq!(d.lod_min_clamp, 0.0); assert_eq!(d.lod_max_clamp, 32.0); } @@ -1044,6 +1051,6 @@ mod tests { assert_eq!(d.address_mode_w, wgpu::AddressMode::ClampToEdge); assert_eq!(d.mag_filter, wgpu::FilterMode::Linear); assert_eq!(d.min_filter, wgpu::FilterMode::Linear); - assert_eq!(d.mipmap_filter, wgpu::FilterMode::Linear); + assert_eq!(d.mipmap_filter, wgpu::MipmapFilterMode::Linear); } } diff --git a/crates/lambda-rs/examples/push_constants.rs b/crates/lambda-rs/examples/immediates.rs similarity index 88% rename from crates/lambda-rs/examples/push_constants.rs rename to crates/lambda-rs/examples/immediates.rs index 06869e52..eb277b10 100644 --- a/crates/lambda-rs/examples/push_constants.rs +++ b/crates/lambda-rs/examples/immediates.rs @@ -16,10 +16,7 @@ use lambda::{ Mesh, MeshBuilder, }, - pipeline::{ - PipelineStage, - RenderPipelineBuilder, - }, + pipeline::RenderPipelineBuilder, render_pass::RenderPassBuilder, scene_math::{ compute_model_view_projection_matrix_about_pivot, @@ -84,20 +81,22 @@ void main() { "#; -// ------------------------------ PUSH CONSTANTS ------------------------------- +// ------------------------------ IMMEDIATES ---------------------------------- +/// Immediate data structure passed to shaders via wgpu's immediates feature. +/// In GLSL shaders, this is still declared as `push_constant` uniform block. #[repr(C)] #[derive(Debug, Clone, Copy)] -pub struct PushConstant { +pub struct ImmediateData { data: [f32; 4], render_matrix: [[f32; 4]; 4], } -pub fn push_constants_to_bytes(push_constants: &PushConstant) -> &[u32] { +pub fn immediate_data_to_bytes(immediate: &ImmediateData) -> &[u32] { let bytes = unsafe { - let size_in_bytes = std::mem::size_of::(); + let size_in_bytes = std::mem::size_of::(); let size_in_u32 = size_in_bytes / std::mem::size_of::(); - let ptr = push_constants as *const PushConstant as *const u32; + let ptr = immediate as *const ImmediateData as *const u32; std::slice::from_raw_parts(ptr, size_in_u32) }; @@ -110,7 +109,7 @@ pub fn push_constants_to_bytes(push_constants: &PushConstant) -> &[u32] { const ROTATION_TURNS_PER_SECOND: f32 = 0.12; -pub struct PushConstantsExample { +pub struct ImmediatesExample { elapsed_seconds: f32, shader: Shader, fs: Shader, @@ -121,7 +120,7 @@ pub struct PushConstantsExample { height: u32, } -impl Component for PushConstantsExample { +impl Component for ImmediatesExample { fn on_attach( &mut self, render_context: &mut lambda::render::RenderContext, @@ -131,7 +130,7 @@ impl Component for PushConstantsExample { render_context.surface_format(), render_context.depth_format(), ); - let push_constant_size = std::mem::size_of::() as u32; + let immediate_data_size = std::mem::size_of::() as u32; // Create triangle mesh. let vertices = [ @@ -190,7 +189,7 @@ impl Component for PushConstantsExample { let pipeline = RenderPipelineBuilder::new() .with_culling(lambda::render::pipeline::CullingMode::None) - .with_push_constant(PipelineStage::VERTEX, push_constant_size) + .with_immediate_data(immediate_data_size) .with_buffer( BufferBuilder::build_from_mesh(&mesh, render_context.gpu()) .expect("Failed to create buffer"), @@ -301,11 +300,10 @@ impl Component for PushConstantsExample { pipeline: render_pipeline.clone(), buffer: 0, }, - RenderCommand::PushConstants { + RenderCommand::Immediates { pipeline: render_pipeline.clone(), - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_bytes(&PushConstant { + bytes: Vec::from(immediate_data_to_bytes(&ImmediateData { data: [0.0, 0.0, 0.0, 0.0], // Transpose to match GPU's column‑major expectation. render_matrix: mesh_matrix.transpose(), @@ -320,20 +318,20 @@ impl Component for PushConstantsExample { } } -impl Default for PushConstantsExample { +impl Default for ImmediatesExample { fn default() -> Self { let triangle_in_3d = VirtualShader::Source { source: VERTEX_SHADER_SOURCE.to_string(), kind: ShaderKind::Vertex, entry_point: "main".to_string(), - name: "push_constants".to_string(), + name: "immediates".to_string(), }; let triangle_fragment_shader = VirtualShader::Source { source: FRAGMENT_SHADER_SOURCE.to_string(), kind: ShaderKind::Fragment, entry_point: "main".to_string(), - name: "push_constants".to_string(), + name: "immediates".to_string(), }; let mut builder = ShaderBuilder::new(); @@ -354,16 +352,16 @@ impl Default for PushConstantsExample { } fn main() { - let runtime = ApplicationRuntimeBuilder::new("3D Push Constants Example") + let runtime = ApplicationRuntimeBuilder::new("3D Immediates Example") .with_window_configured_as(move |window_builder| { return window_builder .with_dimensions(800, 600) - .with_name("3D Push Constants Example"); + .with_name("3D Immediates Example"); }) .with_renderer_configured_as(|renderer_builder| { return renderer_builder.with_render_timeout(1_000_000_000); }) - .with_component(move |runtime, triangles: PushConstantsExample| { + .with_component(move |runtime, triangles: ImmediatesExample| { return (runtime, triangles); }) .build(); diff --git a/crates/lambda-rs/examples/reflective_room.rs b/crates/lambda-rs/examples/reflective_room.rs index 08c91a2c..66b4bc82 100644 --- a/crates/lambda-rs/examples/reflective_room.rs +++ b/crates/lambda-rs/examples/reflective_room.rs @@ -31,7 +31,6 @@ use lambda::{ pipeline::{ CompareFunction, CullingMode, - PipelineStage, RenderPipelineBuilder, StencilFaceState, StencilOperation, @@ -129,20 +128,20 @@ void main() { // (No extra fragment shaders needed; the floor mask uses a vertex-only pipeline.) -// ------------------------------ PUSH CONSTANTS ------------------------------- +// -------------------------------- IMMEDIATES --------------------------------- #[repr(C)] #[derive(Debug, Clone, Copy)] -pub struct PushConstant { +pub struct ImmediateData { mvp: [[f32; 4]; 4], model: [[f32; 4]; 4], } -pub fn push_constants_to_words(push_constants: &PushConstant) -> &[u32] { +pub fn immediate_data_to_words(immediate_data: &ImmediateData) -> &[u32] { unsafe { - let size_in_bytes = std::mem::size_of::(); + let size_in_bytes = std::mem::size_of::(); let size_in_u32 = size_in_bytes / std::mem::size_of::(); - let ptr = push_constants as *const PushConstant as *const u32; + let ptr = immediate_data as *const ImmediateData as *const u32; return std::slice::from_raw_parts(ptr, size_in_u32); } } @@ -406,11 +405,10 @@ impl Component for ReflectiveRoomExample { pipeline: pipe_floor_mask, buffer: 0, }); - cmds.push(RenderCommand::PushConstants { + cmds.push(RenderCommand::Immediates { pipeline: pipe_floor_mask, - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_words(&PushConstant { + bytes: Vec::from(immediate_data_to_words(&ImmediateData { mvp: mvp_floor.transpose(), model: model_floor.transpose(), })), @@ -440,11 +438,10 @@ impl Component for ReflectiveRoomExample { pipeline: pipe_reflected, buffer: 0, }); - cmds.push(RenderCommand::PushConstants { + cmds.push(RenderCommand::Immediates { pipeline: pipe_reflected, - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_words(&PushConstant { + bytes: Vec::from(immediate_data_to_words(&ImmediateData { mvp: mvp_reflect.transpose(), model: model_reflect.transpose(), })), @@ -467,11 +464,10 @@ impl Component for ReflectiveRoomExample { pipeline: pipe_floor_visual, buffer: 0, }); - cmds.push(RenderCommand::PushConstants { + cmds.push(RenderCommand::Immediates { pipeline: pipe_floor_visual, - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_words(&PushConstant { + bytes: Vec::from(immediate_data_to_words(&ImmediateData { mvp: mvp_floor.transpose(), model: model_floor.transpose(), })), @@ -491,11 +487,10 @@ impl Component for ReflectiveRoomExample { pipeline: pipe_normal, buffer: 0, }); - cmds.push(RenderCommand::PushConstants { + cmds.push(RenderCommand::Immediates { pipeline: pipe_normal, - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_words(&PushConstant { + bytes: Vec::from(immediate_data_to_words(&ImmediateData { mvp: mvp.transpose(), model: model.transpose(), })), @@ -576,7 +571,7 @@ impl ReflectiveRoomExample { } let cube_mesh = self.cube_mesh.as_ref().unwrap(); let floor_mesh = self.floor_mesh.as_ref().unwrap(); - let push_constants_size = std::mem::size_of::() as u32; + let immediate_data_size = std::mem::size_of::() as u32; // Build pass descriptions locally first let rp_mask_desc = if self.stencil_enabled { @@ -625,7 +620,7 @@ impl ReflectiveRoomExample { .with_depth_format(DepthFormat::Depth24PlusStencil8) .with_depth_write(false) .with_depth_compare(CompareFunction::Always) - .with_push_constant(PipelineStage::VERTEX, push_constants_size) + .with_immediate_data(immediate_data_size) .with_buffer( BufferBuilder::new() .with_length( @@ -677,7 +672,7 @@ impl ReflectiveRoomExample { // Mirrored transform reverses winding; cull front to keep visible faces. .with_culling(CullingMode::Front) .with_depth_format(DepthFormat::Depth24PlusStencil8) - .with_push_constant(PipelineStage::VERTEX, push_constants_size) + .with_immediate_data(immediate_data_size) .with_buffer( BufferBuilder::new() .with_length( @@ -730,7 +725,7 @@ impl ReflectiveRoomExample { let mut floor_builder = RenderPipelineBuilder::new() .with_label("floor-visual") .with_culling(CullingMode::Back) - .with_push_constant(PipelineStage::VERTEX, push_constants_size) + .with_immediate_data(immediate_data_size) .with_buffer( BufferBuilder::new() .with_length( @@ -768,7 +763,7 @@ impl ReflectiveRoomExample { let mut normal_builder = RenderPipelineBuilder::new() .with_label("cube-normal") .with_culling(CullingMode::Back) - .with_push_constant(PipelineStage::VERTEX, push_constants_size) + .with_immediate_data(immediate_data_size) .with_buffer( BufferBuilder::new() .with_length( diff --git a/crates/lambda-rs/examples/textured_cube.rs b/crates/lambda-rs/examples/textured_cube.rs index 8255aed9..202a3802 100644 --- a/crates/lambda-rs/examples/textured_cube.rs +++ b/crates/lambda-rs/examples/textured_cube.rs @@ -1,7 +1,7 @@ #![allow(clippy::needless_return)] //! Example: Spinning 3D cube sampled with a 3D texture. -//! - Uses MVP push constants (vertex stage) for classic camera + rotation. +//! - Uses MVP immediates (vertex stage) for classic camera + rotation. //! - Colors come from a 2D checkerboard texture sampled in the fragment //! shader. Each face projects model-space coordinates to UVs. @@ -20,10 +20,7 @@ use lambda::{ Mesh, MeshBuilder, }, - pipeline::{ - PipelineStage, - RenderPipelineBuilder, - }, + pipeline::RenderPipelineBuilder, render_pass::RenderPassBuilder, scene_math::{ compute_perspective_projection, @@ -130,20 +127,20 @@ void main() { "#; -// ------------------------------ PUSH CONSTANTS ------------------------------- +// -------------------------------- IMMEDIATES --------------------------------- #[repr(C)] #[derive(Debug, Clone, Copy)] -pub struct PushConstant { +pub struct ImmediateData { mvp: [[f32; 4]; 4], model: [[f32; 4]; 4], } -pub fn push_constants_to_bytes(push_constants: &PushConstant) -> &[u32] { +pub fn immediate_data_to_bytes(immediate_data: &ImmediateData) -> &[u32] { unsafe { - let size_in_bytes = std::mem::size_of::(); + let size_in_bytes = std::mem::size_of::(); let size_in_u32 = size_in_bytes / std::mem::size_of::(); - let ptr = push_constants as *const PushConstant as *const u32; + let ptr = immediate_data as *const ImmediateData as *const u32; std::slice::from_raw_parts(ptr, size_in_u32) } } @@ -330,11 +327,11 @@ impl Component for TexturedCubeExample { .with_sampler(2, &sampler) .build(render_context.gpu()); - let push_constants_size = std::mem::size_of::() as u32; + let immediate_data_size = std::mem::size_of::() as u32; let pipeline = RenderPipelineBuilder::new() .with_culling(lambda::render::pipeline::CullingMode::Back) .with_depth() - .with_push_constant(PipelineStage::VERTEX, push_constants_size) + .with_immediate_data(immediate_data_size) .with_buffer( BufferBuilder::build_from_mesh(&mesh, render_context.gpu()) .expect("Failed to create vertex buffer"), @@ -460,11 +457,10 @@ impl Component for TexturedCubeExample { pipeline, buffer: 0, }, - RenderCommand::PushConstants { + RenderCommand::Immediates { pipeline, - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_bytes(&PushConstant { + bytes: Vec::from(immediate_data_to_bytes(&ImmediateData { mvp: mvp.transpose(), model: model.transpose(), })), diff --git a/crates/lambda-rs/examples/triangles.rs b/crates/lambda-rs/examples/triangles.rs index ddd5b92a..696815e4 100644 --- a/crates/lambda-rs/examples/triangles.rs +++ b/crates/lambda-rs/examples/triangles.rs @@ -11,7 +11,6 @@ use lambda::{ command::RenderCommand, pipeline::{ self, - PipelineStage, }, render_pass, shader::{ @@ -52,10 +51,10 @@ impl Component for TrianglesComponent { render_context.depth_format(), ); - let push_constants_size = std::mem::size_of::() as u32; + let immediate_data_size = std::mem::size_of::() as u32; let pipeline = pipeline::RenderPipelineBuilder::new() .with_culling(pipeline::CullingMode::None) - .with_push_constant(PipelineStage::VERTEX, push_constants_size) + .with_immediate_data(immediate_data_size) .build( render_context.gpu(), render_context.surface_format(), @@ -89,7 +88,7 @@ impl Component for TrianglesComponent { let (x, y) = self.position; let triangle_data = &[ - PushConstant { + ImmediateData { color: [ 1.0, 1.0 * self.animation_scalar, @@ -99,17 +98,17 @@ impl Component for TrianglesComponent { pos: [x, y], scale: [0.3, 0.3], }, - PushConstant { + ImmediateData { color: [0.0, 1.0, 0.0, 1.0], pos: [0.5, 0.0], scale: [0.4, 0.4], }, - PushConstant { + ImmediateData { color: [0.0, 0.0, 1.0, 1.0], pos: [0.25, 0.5], scale: [0.5, 0.5], }, - PushConstant { + ImmediateData { color: [1.0, 1.0, 1.0, 1.0], pos: [0.0, 0.0], scale: [0.5, 0.5], @@ -144,11 +143,10 @@ impl Component for TrianglesComponent { // Upload triangle data into the the GPU at the vertex stage of the pipeline // before requesting to draw each triangle. for triangle in triangle_data { - commands.push(RenderCommand::PushConstants { + commands.push(RenderCommand::Immediates { pipeline: render_pipeline.clone(), - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_bytes(triangle)), + bytes: Vec::from(immediate_data_to_bytes(triangle)), }); commands.push(RenderCommand::Draw { vertices: 0..3, @@ -222,17 +220,17 @@ impl Component for TrianglesComponent { #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct PushConstant { +pub struct ImmediateData { color: [f32; 4], pos: [f32; 2], scale: [f32; 2], } -pub fn push_constants_to_bytes(push_constants: &PushConstant) -> &[u32] { +pub fn immediate_data_to_bytes(immediate_data: &ImmediateData) -> &[u32] { let bytes = unsafe { - let size_in_bytes = std::mem::size_of::(); + let size_in_bytes = std::mem::size_of::(); let size_in_u32 = size_in_bytes / std::mem::size_of::(); - let ptr = push_constants as *const PushConstant as *const u32; + let ptr = immediate_data as *const ImmediateData as *const u32; std::slice::from_raw_parts(ptr, size_in_u32) }; diff --git a/crates/lambda-rs/src/render/command.rs b/crates/lambda-rs/src/render/command.rs index 05398c32..d3b6825b 100644 --- a/crates/lambda-rs/src/render/command.rs +++ b/crates/lambda-rs/src/render/command.rs @@ -7,10 +7,7 @@ use std::ops::Range; -use super::{ - pipeline::PipelineStage, - viewport::Viewport, -}; +use super::viewport::Viewport; /// Engine-level index format for indexed drawing. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -80,13 +77,14 @@ pub enum RenderCommand { /// Set the stencil reference value for the active pass. SetStencilReference { reference: u32 }, - /// Upload push constants for the active pipeline/stage at `offset`. + /// Upload immediate data at `offset`. /// /// The byte vector is interpreted as tightly packed `u32` words; the - /// builder turns it into raw bytes when encoding. - PushConstants { + /// encoder turns it into raw bytes when encoding. Both offset and data + /// length must be multiples of 4 bytes. The GLSL syntax for declaring + /// immediate data blocks remains `layout(push_constant)`. + Immediates { pipeline: super::ResourceId, - stage: PipelineStage, offset: u32, bytes: Vec, }, diff --git a/crates/lambda-rs/src/render/encoder.rs b/crates/lambda-rs/src/render/encoder.rs index e13a206f..469d947a 100644 --- a/crates/lambda-rs/src/render/encoder.rs +++ b/crates/lambda-rs/src/render/encoder.rs @@ -536,16 +536,6 @@ impl<'pass> RenderPassEncoder<'pass> { return Ok(()); } - /// Set push constants for a pipeline stage. - pub fn set_push_constants( - &mut self, - stage: pipeline::PipelineStage, - offset: u32, - data: &[u8], - ) { - self.pass.set_push_constants(stage, offset, data); - } - /// Issue a non-indexed draw call. pub fn draw( &mut self, @@ -656,6 +646,16 @@ impl<'pass> RenderPassEncoder<'pass> { self.pass.draw_indexed(indices, base_vertex, instances); return Ok(()); } + + /// Set immediate data for subsequent draw calls. + /// + /// The `offset` and `data` length MUST be multiples of 4 bytes + /// (IMMEDIATE_DATA_ALIGNMENT). The data bytes are passed directly to the + /// GPU for use by shaders that declare `push_constant` uniform blocks + /// (the GLSL syntax remains unchanged). + pub fn set_immediates(&mut self, offset: u32, data: &[u8]) { + self.pass.set_immediates(offset, data); + } } impl std::fmt::Debug for RenderPassEncoder<'_> { diff --git a/crates/lambda-rs/src/render/gpu.rs b/crates/lambda-rs/src/render/gpu.rs index 2b1036e1..9bf50328 100644 --- a/crates/lambda-rs/src/render/gpu.rs +++ b/crates/lambda-rs/src/render/gpu.rs @@ -206,7 +206,7 @@ impl GpuBuilder { /// /// Defaults: /// - High performance power preference - /// - Push constants enabled + /// - Immediates enabled /// - Performance-oriented memory hints pub fn new() -> Self { return GpuBuilder { diff --git a/crates/lambda-rs/src/render/mod.rs b/crates/lambda-rs/src/render/mod.rs index 0d0bc0e8..939f6f92 100644 --- a/crates/lambda-rs/src/render/mod.rs +++ b/crates/lambda-rs/src/render/mod.rs @@ -9,7 +9,7 @@ //! command encoding. //! - `RenderPass` and `RenderPipeline`: immutable descriptions used when //! beginning a pass and binding a pipeline. Pipelines declare their vertex -//! inputs, push constants, and layout (bind group layouts). +//! inputs, immediate data, and layout (bind group layouts). //! - `Buffer`, `BindGroupLayout`, and `BindGroup`: GPU resources created via //! builders and attached to the context, then referenced by small integer //! handles when encoding commands. @@ -820,6 +820,18 @@ impl RenderContext { return Ok(()); } + fn validate_pipeline_exists( + render_pipelines: &[RenderPipeline], + pipeline: usize, + ) -> Result<(), RenderPassError> { + if render_pipelines.get(pipeline).is_none() { + return Err(RenderPassError::Validation(format!( + "Unknown pipeline {pipeline}" + ))); + } + return Ok(()); + } + fn encode_active_render_pass_commands( command_iter: &mut std::vec::IntoIter, rp_encoder: &mut encoder::RenderPassEncoder<'_>, @@ -895,22 +907,21 @@ impl RenderContext { })?; rp_encoder.set_index_buffer(buffer_ref, format)?; } - RenderCommand::PushConstants { + RenderCommand::Immediates { pipeline, - stage, offset, bytes, } => { - let _ = render_pipelines.get(pipeline).ok_or_else(|| { - RenderPassError::Validation(format!("Unknown pipeline {pipeline}")) - })?; - let slice = unsafe { + Self::validate_pipeline_exists(render_pipelines, pipeline)?; + + // Convert the u32 words to a byte slice for set_immediates. + let byte_slice = unsafe { std::slice::from_raw_parts( bytes.as_ptr() as *const u8, bytes.len() * std::mem::size_of::(), ) }; - rp_encoder.set_push_constants(stage, offset, slice); + rp_encoder.set_immediates(offset, byte_slice); } RenderCommand::Draw { vertices, @@ -1041,4 +1052,13 @@ mod tests { let has_attachment = RenderContext::has_depth_attachment(None, stencil_ops); assert!(has_attachment); } + + #[test] + fn immediates_validate_pipeline_exists_rejects_unknown_pipeline() { + let pipelines: Vec = vec![]; + let err = RenderContext::validate_pipeline_exists(&pipelines, 7) + .err() + .expect("must error"); + assert!(err.to_string().contains("Unknown pipeline 7")); + } } diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index a096434d..108b5b2b 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -15,20 +15,17 @@ //! //! Example //! ```rust,ignore -//! // Single vertex buffer with position/color; one push constant range for the vertex stage -//! use lambda::render::pipeline::{RenderPipelineBuilder, PipelineStage, CullingMode}; +//! // Single vertex buffer with position/color; one immediate data range for the vertex stage +//! use lambda::render::pipeline::{RenderPipelineBuilder, CullingMode}; //! let pipeline = RenderPipelineBuilder::new() //! .with_buffer(vertex_buffer, attributes) -//! .with_push_constant(PipelineStage::VERTEX, 64) +//! .with_immediate_data(64) //! .with_layouts(&[&globals_bgl]) //! .with_culling(CullingMode::Back) //! .build(&mut render_context, &render_pass, &vs, Some(&fs)); //! ``` -use std::{ - ops::Range, - rc::Rc, -}; +use std::rc::Rc; use lambda_platform::wgpu::pipeline as platform_pipeline; use logging; @@ -116,12 +113,12 @@ impl RenderPipeline { } } -/// Public alias for platform shader stage flags used by push constants. +/// Public alias for platform shader stage flags. +/// +/// Stage flags remain useful for APIs such as bind group visibility, even +/// though wgpu v28 immediates no longer use stage-scoped updates. pub use platform_pipeline::PipelineStage; -/// Convenience alias for uploading push constants: stage and byte range. -pub type PushConstantUpload = (PipelineStage, Range); - struct BufferBinding { buffer: Rc, layout: VertexBufferLayout, @@ -260,7 +257,7 @@ pub struct StencilState { /// - If a fragment shader is omitted, no color target is attached and the /// pipeline can still be used for vertex‑only workloads. pub struct RenderPipelineBuilder { - push_constants: Vec, + immediate_data: Vec>, bindings: Vec, culling: CullingMode, bind_group_layouts: Vec, @@ -277,7 +274,7 @@ impl RenderPipelineBuilder { /// Creates a new render pipeline builder. pub fn new() -> Self { Self { - push_constants: Vec::new(), + immediate_data: Vec::new(), bindings: Vec::new(), culling: CullingMode::Back, bind_group_layouts: Vec::new(), @@ -346,13 +343,14 @@ impl RenderPipelineBuilder { ); } - /// Declare a push constant range for a shader stage in bytes. - pub fn with_push_constant( - mut self, - stage: PipelineStage, - bytes: u32, - ) -> Self { - self.push_constants.push((stage, 0..bytes)); + /// Declare an immediate data byte range size. + /// + /// wgpu v28 uses a single immediate data region sized by the pipeline + /// layout. This method records a range starting at 0 whose end defines the + /// required allocation size. Multiple calls are allowed; the final + /// allocation is derived from the union of ranges. + pub fn with_immediate_data(mut self, bytes: u32) -> Self { + self.immediate_data.push(0..bytes); return self; } @@ -432,7 +430,7 @@ impl RenderPipelineBuilder { } /// Build a graphics pipeline using the provided shader modules and - /// previously registered vertex inputs and push constants. + /// previously registered vertex inputs and immediate data. /// /// # Arguments /// * `gpu` - The GPU device to create the pipeline on. @@ -464,15 +462,15 @@ impl RenderPipelineBuilder { ) }); - // Push constant ranges - let push_constant_ranges: Vec = self - .push_constants - .iter() - .map(|(stage, range)| platform_pipeline::PushConstantRange { - stages: *stage, - range: range.clone(), - }) - .collect(); + // Immediate data ranges + let immediate_data_ranges: Vec = + self + .immediate_data + .iter() + .map(|range| platform_pipeline::ImmediateDataRange { + range: range.clone(), + }) + .collect(); // Bind group layouts limit check let max_bind_groups = gpu.limit_max_bind_groups() as usize; @@ -535,7 +533,7 @@ impl RenderPipelineBuilder { let pipeline_layout = platform_pipeline::PipelineLayoutBuilder::new() .with_label("lambda-pipeline-layout") .with_layouts(&bgl_platform) - .with_push_constants(push_constant_ranges) + .with_immediate_data_ranges(immediate_data_ranges) .build(gpu.platform()); // Vertex buffers and attributes diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index c86d8162..00ef8016 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -3,10 +3,10 @@ title: "Tutorials Index" document_id: "tutorials-index-2025-10-17" status: "living" created: "2025-10-17T00:20:00Z" -last_updated: "2025-12-29T00:00:00Z" -version: "0.6.0" +last_updated: "2026-01-05T00:00:00Z" +version: "0.7.0" engine_workspace_version: "2023.1.30" -wgpu_version: "26.0.1" +wgpu_version: "28.0.0" shader_backend_default: "naga" winit_version: "0.29.10" repo_commit: "bc2ca687922db601998e7e5a0c0b2e870c857be1" @@ -18,10 +18,10 @@ tags: ["index", "tutorials", "docs"] This index lists tutorials that teach specific `lambda-rs` tasks through complete, incremental builds. - Basic Triangle: Vertex‑Only Draw — [basic-triangle.md](basic-triangle.md) -- Push Constants: Draw Multiple 2D Triangles — [push-constants-multiple-triangles.md](push-constants-multiple-triangles.md) +- Immediates: Draw Multiple 2D Triangles — [immediates-multiple-triangles.md](immediates-multiple-triangles.md) - Uniform Buffers: Build a Spinning Triangle — [uniform-buffers.md](uniform-buffers.md) - Textured Quad: Sample a 2D Texture — [textured-quad.md](textured-quad.md) -- Textured Cube: 3D Push Constants + 2D Sampling — [textured-cube.md](textured-cube.md) +- Textured Cube: 3D Immediates + 2D Sampling — [textured-cube.md](textured-cube.md) - Offscreen Post: Render to a Texture and Sample to the Surface — [offscreen-post.md](offscreen-post.md) - Reflective Room: Stencil Masked Reflections with MSAA — [reflective-room.md](reflective-room.md) - Instanced Rendering: Grid of Colored Quads — [instanced-quads.md](instanced-quads.md) @@ -29,6 +29,8 @@ This index lists tutorials that teach specific `lambda-rs` tasks through complet Browse all tutorials in this directory. Changelog + +- 0.7.0 (2026-01-05): Rename push constants tutorial to immediates for wgpu v28; update metadata. - 0.6.0 (2025-12-29): Add offscreen post tutorial; update metadata and commit. - 0.5.0 (2025-12-16): Add basic triangle and multi-triangle push constants tutorials; update metadata and commit. - 0.4.0 (2025-11-25): Add Instanced Quads tutorial; update metadata and commit. diff --git a/docs/tutorials/basic-triangle.md b/docs/tutorials/basic-triangle.md index 06de5696..1ac94abd 100644 --- a/docs/tutorials/basic-triangle.md +++ b/docs/tutorials/basic-triangle.md @@ -3,10 +3,10 @@ title: "Basic Triangle: Vertex‑Only Draw" document_id: "basic-triangle-tutorial-2025-12-16" status: "draft" created: "2025-12-16T00:00:00Z" -last_updated: "2025-12-16T00:00:00Z" -version: "0.1.0" +last_updated: "2026-01-05T00:00:00Z" +version: "0.2.0" engine_workspace_version: "2023.1.30" -wgpu_version: "26.0.1" +wgpu_version: "28.0.0" shader_backend_default: "naga" winit_version: "0.29.10" repo_commit: "797047468a927f1e4ba111b43381a607ac53c0d1" @@ -223,8 +223,8 @@ shaders, build a render pass and pipeline, and issue a draw using - Exercise 3: Add a second triangle - Issue a second `Draw` and offset positions in the shader for one of the triangles. -- Exercise 4: Introduce push constants - - Add a push constant color and position and port the shader interface to +- Exercise 4: Introduce immediates + - Add an immediate data block for color and position and port the shader interface to match `crates/lambda-rs/examples/triangles.rs`. - Exercise 5: Replace `gl_VertexIndex` with a vertex buffer - Create a vertex buffer for positions and update the pipeline and shader @@ -232,5 +232,6 @@ shaders, build a render pass and pipeline, and issue a draw using ## Changelog +- 0.2.0 (2026-01-05): Update for wgpu v28; rename push constants to immediates in exercises. - 0.1.0 (2025-12-16): Initial draft aligned with `crates/lambda-rs/examples/triangle.rs`. diff --git a/docs/tutorials/push-constants-multiple-triangles.md b/docs/tutorials/immediates-multiple-triangles.md similarity index 58% rename from docs/tutorials/push-constants-multiple-triangles.md rename to docs/tutorials/immediates-multiple-triangles.md index e8969915..91af8667 100644 --- a/docs/tutorials/push-constants-multiple-triangles.md +++ b/docs/tutorials/immediates-multiple-triangles.md @@ -1,26 +1,30 @@ --- -title: "Push Constants: Draw Multiple 2D Triangles" -document_id: "push-constants-multiple-triangles-tutorial-2025-12-16" +title: "Immediates: Draw Multiple 2D Triangles" +document_id: "immediates-multiple-triangles-tutorial-2025-12-16" status: "draft" created: "2025-12-16T00:00:00Z" -last_updated: "2025-12-16T00:00:00Z" -version: "0.1.0" +last_updated: "2026-01-07T00:00:00Z" +version: "0.2.1" engine_workspace_version: "2023.1.30" -wgpu_version: "26.0.1" +wgpu_version: "28.0.0" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "797047468a927f1e4ba111b43381a607ac53c0d1" +repo_commit: "183a0499250a2c16e0a09b22107201720016fc48" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] -tags: ["tutorial", "graphics", "push-constants", "triangle", "rust", "wgpu"] +tags: ["tutorial", "graphics", "immediates", "triangle", "rust", "wgpu"] --- ## Overview -Push constants provide a small block of per-draw data that is cheap to update -and does not require buffers or bind groups. This tutorial draws multiple 2D -triangles by looping over a set of push constant values and issuing one draw -per triangle. +Immediates (formerly push constants in wgpu < v28) provide a small block of +per-draw data that is cheap to update and does not require buffers or bind +groups. This tutorial draws multiple 2D triangles by looping over a set of +immediate values and issuing one draw per triangle. + +> **Note:** In wgpu v28, push constants were renamed to "immediates" and gated +> behind the `Features::IMMEDIATES` feature flag. The GLSL shaders still use +> `layout(push_constant)` syntax. Reference implementation: `crates/lambda-rs/examples/triangles.rs`. @@ -32,10 +36,10 @@ Reference implementation: `crates/lambda-rs/examples/triangles.rs`. - [Requirements and Constraints](#requirements-and-constraints) - [Data Flow](#data-flow) - [Implementation Steps](#implementation-steps) - - [Step 1 — Define the Push Constant Layout](#step-1) + - [Step 1 — Define the Immediate Data Layout](#step-1) - [Step 2 — Shaders for Position, Scale, and Color](#step-2) - - [Step 3 — Build a Pipeline with Push Constants](#step-3) - - [Step 4 — Push Constants per Draw](#step-4) + - [Step 3 — Build a Pipeline with Immediates](#step-3) + - [Step 4 — Immediates per Draw](#step-4) - [Step 5 — Input and Resize Handling](#step-5) - [Validation](#validation) - [Notes](#notes) @@ -45,9 +49,9 @@ Reference implementation: `crates/lambda-rs/examples/triangles.rs`. ## Goals -- Define a push constant block in GLSL and mirror it in Rust. -- Build a pipeline that declares a vertex-stage push constant range. -- Draw multiple triangles by pushing per-draw constants and issuing draws. +- Define an immediate data block in GLSL (using `push_constant`) and mirror it in Rust. +- Build a pipeline that declares an immediate data range. +- Draw multiple triangles by setting per-draw immediates and issuing draws. ## Prerequisites @@ -56,34 +60,34 @@ Reference implementation: `crates/lambda-rs/examples/triangles.rs`. ## Requirements and Constraints -- Push constant layout MUST match between shader and Rust in size, alignment, +- Immediate data layout MUST match between shader and Rust in size, alignment, and field order (`#[repr(C)]` is required on the Rust struct). -- The pipeline MUST declare a push constant range for the stage that reads it - (`PipelineStage::VERTEX` in this example). -- The pushed byte slice length MUST match the pipeline’s declared push constant - size. +- The pipeline MUST declare an immediate data range that matches the shader + immediate block size. +- The immediate byte slice length MUST match the pipeline's declared size. - Back-face culling MUST be disabled or the triangle winding MUST be adjusted. Rationale: the example’s vertex positions are defined in clockwise order. ## Data Flow - CPU constructs pipeline and render pass once in `on_attach`. -- CPU builds a list of per-triangle `PushConstant` values on each frame. -- CPU emits a loop of `PushConstants` → `Draw` inside a single render pass. +- CPU builds a list of per-triangle `ImmediateData` values on each frame. +- CPU emits a loop of `Immediates` → `Draw` inside a single render pass. ASCII diagram ``` -PushConstant (CPU) ──▶ RenderCommand::PushConstants ──▶ Vertex Shader +ImmediateData (CPU) ──▶ RenderCommand::Immediates ──▶ Vertex Shader │ │ └────────────── per triangle draw ──────────────┘ ``` ## Implementation Steps -### Step 1 — Define the Push Constant Layout +### Step 1 — Define the Immediate Data Layout -Define the push constant block in the vertex shader and mirror it in Rust. +Define the immediate data block in the vertex shader (using `push_constant` +syntax) and mirror it in Rust. ```glsl layout(push_constant) uniform PushConstant { @@ -96,7 +100,7 @@ layout(push_constant) uniform PushConstant { ```rust #[repr(C)] #[derive(Debug, Copy, Clone)] -pub struct PushConstant { +pub struct ImmediateData { color: [f32; 4], pos: [f32; 2], scale: [f32; 2], @@ -111,20 +115,21 @@ The vertex shader uses `gl_VertexIndex` to select base positions, then applies `scale` and `pos`. The fragment shader outputs the interpolated color. Reference shader sources: + - `crates/lambda-rs/assets/shaders/triangles.vert` - `crates/lambda-rs/assets/shaders/triangles.frag` -### Step 3 — Build a Pipeline with Push Constants +### Step 3 — Build a Pipeline with Immediates -Compute the push constant size and configure the pipeline to accept vertex-stage -push constants. Disable culling for consistent visibility. +Compute the immediate data size and configure the pipeline to accept immediate +data. Disable culling for consistent visibility. ```rust -let push_constants_size = std::mem::size_of::() as u32; +let immediate_size = std::mem::size_of::() as u32; let pipeline = pipeline::RenderPipelineBuilder::new() .with_culling(pipeline::CullingMode::None) - .with_push_constant(PipelineStage::VERTEX, push_constants_size) + .with_immediate_data(immediate_size) .build( render_context.gpu(), render_context.surface_format(), @@ -135,20 +140,19 @@ let pipeline = pipeline::RenderPipelineBuilder::new() ); ``` -The pipeline definition controls which stages can read the pushed bytes. +The pipeline definition declares the byte range available as immediates. -### Step 4 — Push Constants per Draw +### Step 4 — Set Immediates per Draw -Build a list of `PushConstant` values, then emit a loop that pushes bytes and -issues a draw for each triangle. +Build a list of `ImmediateData` values, then emit a loop that sets immediate +bytes and issues a draw for each triangle. ```rust for triangle in triangle_data { - commands.push(RenderCommand::PushConstants { + commands.push(RenderCommand::Immediates { pipeline: render_pipeline.clone(), - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_bytes(triangle)), + bytes: Vec::from(immediate_data_to_bytes(triangle)), }); commands.push(RenderCommand::Draw { vertices: 0..3, @@ -162,6 +166,7 @@ This produces multiple triangles without creating any GPU buffers. ### Step 5 — Input and Resize Handling Update component state from events: + - `WindowEvent::Resize` updates the stored width/height for viewport creation. - `KeyW`, `KeyA`, `KeyS`, `KeyD` update the translation for one triangle. @@ -176,11 +181,12 @@ These updates are reflected on the next `on_render` call. ## Notes -- Push constant limits - - Push constants are device-limited; the declared size MUST remain within the - GPU’s supported push constant range. +- Immediate data limits + - Immediate data is device-limited; the declared size MUST remain within the + GPU's supported immediate data range. wgpu v28 requires the `Features::IMMEDIATES` + feature to be enabled. - Layout correctness - - The Rust `PushConstant` type MUST remain `#[repr(C)]` and must not include + - The Rust `ImmediateData` type MUST remain `#[repr(C)]` and must not include padding-sensitive fields without validating the matching GLSL layout. - Naming - The reference implementation stores the fragment shader in the field named @@ -188,9 +194,9 @@ These updates are reflected on the next `on_render` call. ## Conclusion -This tutorial demonstrates per-draw customization using push constants by -looping over a set of constants and issuing repeated draws within one render -pass. +This tutorial demonstrates per-draw customization using immediates (wgpu's v28 +replacement for push constants) by looping over a set of immediate values and +issuing repeated draws within one render pass. ## Exercises @@ -198,7 +204,7 @@ pass. - Update `animation_scalar` each frame and modulate one triangle’s color or scale over time. - Exercise 2: Add per-triangle rotation - - Extend the push constant block with an angle and rotate positions in the + - Extend the immediate data block with an angle and rotate positions in the vertex shader. - Exercise 3: Enable back-face culling - Set `.with_culling(CullingMode::Back)` and update @@ -213,5 +219,7 @@ pass. ## Changelog +- 0.2.1 (2026-01-07): Remove stage usage from immediates API examples. +- 0.2.0 (2026-01-05): Updated to use wgpu v28 immediates terminology. - 0.1.0 (2025-12-16): Initial draft aligned with `crates/lambda-rs/examples/triangles.rs`. diff --git a/docs/tutorials/reflective-room.md b/docs/tutorials/reflective-room.md index 8e51f6ae..ecc0885e 100644 --- a/docs/tutorials/reflective-room.md +++ b/docs/tutorials/reflective-room.md @@ -3,16 +3,16 @@ title: "Reflective Floor: Stencil‑Masked Planar Reflections" document_id: "reflective-room-tutorial-2025-11-17" status: "draft" created: "2025-11-17T00:00:00Z" -last_updated: "2025-12-15T00:00:00Z" -version: "0.3.0" +last_updated: "2026-01-07T00:00:00Z" +version: "0.4.1" engine_workspace_version: "2023.1.30" -wgpu_version: "26.0.1" +wgpu_version: "28.0.0" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" +repo_commit: "183a0499250a2c16e0a09b22107201720016fc48" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] -tags: ["tutorial", "graphics", "stencil", "depth", "msaa", "mirror", "3d", "push-constants", "wgpu", "rust"] +tags: ["tutorial", "graphics", "stencil", "depth", "msaa", "mirror", "3d", "immediates", "wgpu", "rust"] --- ## Overview @@ -30,7 +30,7 @@ Reference implementation: `crates/lambda-rs/examples/reflective_room.rs`. - [Data Flow](#data-flow) - [Implementation Steps](#implementation-steps) - [Step 1 — Runtime and Component Skeleton](#step-1) - - [Step 2 — Shaders and Push Constants](#step-2) + - [Step 2 — Shaders and Immediates](#step-2) - [Step 3 — Meshes: Cube and Floor](#step-3) - [Step 4 — Render Passes: Mask and Color](#step-4) - [Step 5 — Pipeline: Floor Mask (Stencil Write)](#step-5) @@ -51,7 +51,7 @@ Reference implementation: `crates/lambda-rs/examples/reflective_room.rs`. - Use the stencil buffer to restrict rendering to the floor area and show a mirrored reflection of a cube. - Support depth testing and 4× MSAA to improve geometric correctness and edge quality. -- Drive transforms via push constants for model‑view‑projection (MVP) and model matrices. +- Drive transforms via immediates for model‑view‑projection (MVP) and model matrices. - Provide runtime toggles for MSAA, stencil, and depth testing, plus camera pitch and visibility helpers. ## Prerequisites @@ -65,7 +65,10 @@ Reference implementation: `crates/lambda-rs/examples/reflective_room.rs`. - The mask pass MUST disable depth writes and write stencil with `Replace` so the floor area becomes `1`. - The reflected cube pipeline MUST test stencil `Equal` against reference `1` and SHOULD set stencil write mask to `0x00`. - Mirroring across the floor plane flips face winding. Culling MUST be disabled for the reflected draw or the front‑face definition MUST be adjusted. This example culls front faces for the reflected cube. -- Push constant size and stage visibility MUST match the shader declaration. Two `mat4` values are sent to the vertex stage only (128 bytes total). +- Immediate data size MUST match the shader declaration. Two `mat4` values are sent (128 bytes total). + +> **Note:** In wgpu v28, push constants were renamed to "immediates" and require the `Features::IMMEDIATES` feature. The GLSL syntax remains `push_constant`. + - Matrix order MUST match the shader’s expectation. The example transposes matrices before upload to match GLSL column‑major multiplication. - The render pass and pipelines MUST use the same sample count when MSAA is enabled. - Acronyms: graphics processing unit (GPU), central processing unit (CPU), multi‑sample anti‑aliasing (MSAA), model‑view‑projection (MVP). @@ -128,9 +131,11 @@ impl Component for ReflectiveRoomExample { /* lifecycle Narrative: The component stores GPU handles and toggles. When settings change, mark `needs_rebuild = true` and rebuild pipelines/passes on the next frame. -### Step 2 — Shaders and Push Constants +### Step 2 — Shaders and Immediates + +Use one vertex shader and two fragment shaders. The vertex shader expects immediates with two `mat4` values: the MVP and the model matrix, used to transform positions and rotate normals to world space. The floor fragment shader is lit and translucent so the reflection reads beneath it. -Use one vertex shader and two fragment shaders. The vertex shader expects push constants with two `mat4` values: the MVP and the model matrix, used to transform positions and rotate normals to world space. The floor fragment shader is lit and translucent so the reflection reads beneath it. +> **Note:** In wgpu v28, push constants were renamed to "immediates" and gated behind `Features::IMMEDIATES`. The GLSL syntax remains `layout(push_constant)`. ```glsl // Vertex (GLSL 450) @@ -174,16 +179,16 @@ void main() { } ``` -In Rust, pack push constants as 32‑bit words and transpose matrices before upload. +In Rust, pack immediate data as 32‑bit words and transpose matrices before upload. ```rust #[repr(C)] -pub struct PushConstant { pub mvp: [[f32; 4]; 4], pub model: [[f32; 4]; 4] } +pub struct ImmediateData { pub mvp: [[f32; 4]; 4], pub model: [[f32; 4]; 4] } -pub fn push_constants_to_words(pc: &PushConstant) -> &[u32] { +pub fn immediate_data_to_words(data: &ImmediateData) -> &[u32] { unsafe { - let size = std::mem::size_of::() / std::mem::size_of::(); - let ptr = pc as *const PushConstant as *const u32; + let size = std::mem::size_of::() / std::mem::size_of::(); + let ptr = data as *const ImmediateData as *const u32; return std::slice::from_raw_parts(ptr, size); } } @@ -233,14 +238,14 @@ Rationale: pipelines that use stencil require a depth‑stencil attachment, even Draw the floor geometry to write `stencil = 1` where the floor covers. Do not write to color. Disable depth writes and set depth compare to `Always`. ```rust -use lambda::render::pipeline::{RenderPipelineBuilder, CompareFunction, StencilState, StencilFaceState, StencilOperation, PipelineStage}; +use lambda::render::pipeline::{RenderPipelineBuilder, CompareFunction, StencilState, StencilFaceState, StencilOperation}; let pipe_floor_mask = RenderPipelineBuilder::new() .with_label("floor-mask") .with_depth_format(lambda::render::texture::DepthFormat::Depth24PlusStencil8) .with_depth_write(false) .with_depth_compare(CompareFunction::Always) - .with_push_constant(PipelineStage::VERTEX, std::mem::size_of::() as u32) + .with_immediate_data(std::mem::size_of::() as u32) .with_buffer(floor_vertex_buffer, floor_attributes) .with_stencil(StencilState { front: StencilFaceState { compare: CompareFunction::Always, fail_op: StencilOperation::Keep, depth_fail_op: StencilOperation::Keep, pass_op: StencilOperation::Replace }, @@ -267,7 +272,7 @@ let mut builder = RenderPipelineBuilder::new() .with_label("reflected-cube") .with_culling(lambda::render::pipeline::CullingMode::Front) .with_depth_format(lambda::render::texture::DepthFormat::Depth24PlusStencil8) - .with_push_constant(PipelineStage::VERTEX, std::mem::size_of::() as u32) + .with_immediate_data(std::mem::size_of::() as u32) .with_buffer(cube_vertex_buffer, cube_attributes) .with_stencil(StencilState { front: StencilFaceState { compare: CompareFunction::Equal, fail_op: StencilOperation::Keep, depth_fail_op: StencilOperation::Keep, pass_op: StencilOperation::Keep }, @@ -295,7 +300,7 @@ Draw the floor surface with a translucent tint so the reflection remains visible ```rust let mut floor_vis = RenderPipelineBuilder::new() .with_label("floor-visual") - .with_push_constant(PipelineStage::VERTEX, std::mem::size_of::() as u32) + .with_immediate_data(std::mem::size_of::() as u32) .with_buffer(floor_vertex_buffer, floor_attributes) .with_multi_sample(msaa_samples); @@ -323,7 +328,7 @@ Draw the unreflected cube above the floor using the lit fragment shader. Enable ```rust let mut normal = RenderPipelineBuilder::new() .with_label("cube-normal") - .with_push_constant(PipelineStage::VERTEX, std::mem::size_of::() as u32) + .with_immediate_data(std::mem::size_of::() as u32) .with_buffer(cube_vertex_buffer, cube_attributes) .with_multi_sample(msaa_samples); @@ -382,7 +387,6 @@ Record commands in the following order. Set `viewport` and `scissor` to the wind ```rust use lambda::render::command::RenderCommand; -use lambda::render::pipeline::PipelineStage; let mut cmds: Vec = Vec::new(); @@ -391,7 +395,7 @@ cmds.push(RenderCommand::BeginRenderPass { render_pass: pass_id_mask, viewport } cmds.push(RenderCommand::SetPipeline { pipeline: pipe_floor_mask }); cmds.push(RenderCommand::SetStencilReference { reference: 1 }); cmds.push(RenderCommand::BindVertexBuffer { pipeline: pipe_floor_mask, buffer: 0 }); -cmds.push(RenderCommand::PushConstants { pipeline: pipe_floor_mask, stage: PipelineStage::VERTEX, offset: 0, bytes: Vec::from(push_constants_to_words(&PushConstant { mvp: mvp_floor.transpose(), model: model_floor.transpose() })) }); +cmds.push(RenderCommand::Immediates { pipeline: pipe_floor_mask, offset: 0, bytes: Vec::from(immediate_data_to_words(&ImmediateData { mvp: mvp_floor.transpose(), model: model_floor.transpose() })) }); cmds.push(RenderCommand::Draw { vertices: 0..floor_vertex_count, instances: 0..1 }); cmds.push(RenderCommand::EndRenderPass); @@ -400,19 +404,19 @@ cmds.push(RenderCommand::BeginRenderPass { render_pass: pass_id_color, viewport cmds.push(RenderCommand::SetPipeline { pipeline: pipe_reflected }); cmds.push(RenderCommand::SetStencilReference { reference: 1 }); cmds.push(RenderCommand::BindVertexBuffer { pipeline: pipe_reflected, buffer: 0 }); -cmds.push(RenderCommand::PushConstants { pipeline: pipe_reflected, stage: PipelineStage::VERTEX, offset: 0, bytes: Vec::from(push_constants_to_words(&PushConstant { mvp: mvp_reflect.transpose(), model: model_reflect.transpose() })) }); +cmds.push(RenderCommand::Immediates { pipeline: pipe_reflected, offset: 0, bytes: Vec::from(immediate_data_to_words(&ImmediateData { mvp: mvp_reflect.transpose(), model: model_reflect.transpose() })) }); cmds.push(RenderCommand::Draw { vertices: 0..cube_vertex_count, instances: 0..1 }); // Pass 3: floor visual (tinted) cmds.push(RenderCommand::SetPipeline { pipeline: pipe_floor_visual }); cmds.push(RenderCommand::BindVertexBuffer { pipeline: pipe_floor_visual, buffer: 0 }); -cmds.push(RenderCommand::PushConstants { pipeline: pipe_floor_visual, stage: PipelineStage::VERTEX, offset: 0, bytes: Vec::from(push_constants_to_words(&PushConstant { mvp: mvp_floor.transpose(), model: model_floor.transpose() })) }); +cmds.push(RenderCommand::Immediates { pipeline: pipe_floor_visual, offset: 0, bytes: Vec::from(immediate_data_to_words(&ImmediateData { mvp: mvp_floor.transpose(), model: model_floor.transpose() })) }); cmds.push(RenderCommand::Draw { vertices: 0..floor_vertex_count, instances: 0..1 }); // Pass 4: normal cube cmds.push(RenderCommand::SetPipeline { pipeline: pipe_normal }); cmds.push(RenderCommand::BindVertexBuffer { pipeline: pipe_normal, buffer: 0 }); -cmds.push(RenderCommand::PushConstants { pipeline: pipe_normal, stage: PipelineStage::VERTEX, offset: 0, bytes: Vec::from(push_constants_to_words(&PushConstant { mvp: mvp.transpose(), model: model.transpose() })) }); +cmds.push(RenderCommand::Immediates { pipeline: pipe_normal, offset: 0, bytes: Vec::from(immediate_data_to_words(&ImmediateData { mvp: mvp.transpose(), model: model.transpose() })) }); cmds.push(RenderCommand::Draw { vertices: 0..cube_vertex_count, instances: 0..1 }); cmds.push(RenderCommand::EndRenderPass); ``` @@ -473,6 +477,8 @@ The reflective floor combines a simple stencil mask with an optional depth test ## Changelog +- 2026-01-07, 0.4.1: Remove stage usage from immediates API examples. +- 2026-01-05, 0.4.0: Update for wgpu v28; rename push constants to immediates; update struct references to `ImmediateData`. - 2025-12-15, 0.3.0: Update builder API calls to use `ctx.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 2025-11-21, 0.2.2: Align tutorial with removal of the unmasked reflection debug toggle in the example and update metadata to the current engine workspace commit. - 0.2.0 (2025‑11‑19): Updated for camera pitch, front‑face culling on reflection, lit translucent floor, unmasked reflection debug toggle, floor overlay toggle, and Metal portability note. diff --git a/docs/tutorials/textured-cube.md b/docs/tutorials/textured-cube.md index 37520655..f80757a1 100644 --- a/docs/tutorials/textured-cube.md +++ b/docs/tutorials/textured-cube.md @@ -1,23 +1,23 @@ --- -title: "Textured Cube: 3D Push Constants + 2D Sampling" +title: "Textured Cube: 3D Immediates + 2D Sampling" document_id: "textured-cube-tutorial-2025-11-10" status: "draft" created: "2025-11-10T00:00:00Z" -last_updated: "2025-12-15T00:00:00Z" -version: "0.2.0" +last_updated: "2026-01-07T00:00:00Z" +version: "0.3.1" engine_workspace_version: "2023.1.30" -wgpu_version: "26.0.1" +wgpu_version: "28.0.0" shader_backend_default: "naga" winit_version: "0.29.10" -repo_commit: "71256389b9efe247a59aabffe9de58147b30669d" +repo_commit: "183a0499250a2c16e0a09b22107201720016fc48" owners: ["lambda-sh"] reviewers: ["engine", "rendering"] -tags: ["tutorial", "graphics", "3d", "push-constants", "textures", "samplers", "rust", "wgpu"] +tags: ["tutorial", "graphics", "3d", "immediates", "textures", "samplers", "rust", "wgpu"] --- ## Overview -This tutorial builds a spinning 3D cube that uses push constants to provide model‑view‑projection (MVP) and model matrices to the vertex shader, and samples a 2D checkerboard texture in the fragment shader. Depth testing and back‑face culling are enabled so hidden faces do not render. +This tutorial builds a spinning 3D cube that uses immediates to provide model‑view‑projection (MVP) and model matrices to the vertex shader, and samples a 2D checkerboard texture in the fragment shader. Depth testing and back‑face culling are enabled so hidden faces do not render. Reference implementation: `crates/lambda-rs/examples/textured_cube.rs`. @@ -30,14 +30,14 @@ Reference implementation: `crates/lambda-rs/examples/textured_cube.rs`. - [Data Flow](#data-flow) - [Implementation Steps](#implementation-steps) - [Step 1 — Runtime and Component Skeleton](#step-1) - - [Step 2 — Shaders with Push Constants](#step-2) + - [Step 2 — Shaders with Immediates](#step-2) - [Step 3 — Cube Mesh and Vertex Layout](#step-3) - [Step 4 — Build a 2D Checkerboard Texture](#step-4) - [Step 5 — Create a Sampler](#step-5) - [Step 6 — Bind Group Layout and Bind Group](#step-6) - [Step 7 — Render Pipeline with Depth and Culling](#step-7) - [Step 8 — Per‑Frame Camera and Transforms](#step-8) - - [Step 9 — Record Draw Commands with Push Constants](#step-9) + - [Step 9 — Record Draw Commands with Immediates](#step-9) - [Step 10 — Handle Window Resize](#step-10) - [Validation](#validation) - [Notes](#notes) @@ -49,7 +49,7 @@ Reference implementation: `crates/lambda-rs/examples/textured_cube.rs`. ## Goals - Render a rotating cube with correct occlusion using depth testing and back‑face culling. -- Pass model‑view‑projection (MVP) and model matrices via push constants to the vertex stage. +- Pass model‑view‑projection (MVP) and model matrices via immediates to the vertex stage. - Sample a 2D texture in the fragment stage using a separate sampler, and apply simple Lambert lighting to emphasize shape. ## Prerequisites @@ -59,8 +59,8 @@ Reference implementation: `crates/lambda-rs/examples/textured_cube.rs`. ## Requirements and Constraints -- Push constant size and stage visibility MUST match the shader declaration. This example sends 128 bytes (two `mat4`), at vertex stage only. -- The push constant byte order MUST match the shader’s expected matrix layout. This example transposes matrices before upload to match column‑major multiplication in GLSL. +- Immediate data size MUST match the shader declaration. This example sends 128 bytes (two `mat4`). +- The immediate data byte order MUST match the shader's expected matrix layout. This example transposes matrices before upload to match column‑major multiplication in GLSL. - Face winding MUST be counter‑clockwise (CCW) for back‑face culling to work with `CullingMode::Back`. - The model matrix MUST NOT include non‑uniform scale if normals are transformed with `mat3(model)`. Rationale: non‑uniform scale skews normals; either avoid it or compute a proper normal matrix. - Binding indices MUST match between Rust and shaders: set 0, binding 1 is the 2D texture; set 0, binding 2 is the sampler. @@ -78,8 +78,8 @@ TextureBuilder (2D sRGB) + SamplerBuilder (linear clamp) BindGroup(set0): binding1=texture2D, binding2=sampler │ ▲ ▼ │ -Render Pipeline (vertex: push constants, fragment: sampling) - │ MVP + model (push constants) │ +Render Pipeline (vertex: immediates, fragment: sampling) + │ MVP + model (immediates) │ ▼ │ Render Pass (depth enabled, back‑face culling) → Draw 36 vertices ``` @@ -155,9 +155,11 @@ fn main() { This scaffold establishes the runtime and stores component state required to create resources and animate the cube. -### Step 2 — Shaders with Push Constants +### Step 2 — Shaders with Immediates -Define GLSL 450 shaders. The vertex shader declares a push constant block with two `mat4` values: `mvp` and `model`. The fragment shader samples a 2D texture using a separate sampler and applies simple Lambert lighting for shape definition. +Define GLSL 450 shaders. The vertex shader declares an immediate data block (using `push_constant` syntax) with two `mat4` values: `mvp` and `model`. The fragment shader samples a 2D texture using a separate sampler and applies simple Lambert lighting for shape definition. + +> **Note:** In wgpu v28, push constants were renamed to "immediates" and require the `Features::IMMEDIATES` feature. The GLSL syntax remains `push_constant`. ```glsl // Vertex (GLSL 450) @@ -351,12 +353,12 @@ let bind_group = BindGroupBuilder::new() ### Step 7 — Render Pipeline with Depth and Culling -Enable depth, back‑face culling, and declare a vertex buffer built from the mesh. Add a push constant range for the vertex stage. +Enable depth, back‑face culling, and declare a vertex buffer built from the mesh. Add an immediate data range for the shaders. ```rust use lambda::render::{ buffer::BufferBuilder, - pipeline::{PipelineStage, RenderPipelineBuilder, CullingMode}, + pipeline::{RenderPipelineBuilder, CullingMode}, render_pass::RenderPassBuilder, }; @@ -369,12 +371,12 @@ let render_pass = RenderPassBuilder::new() render_context.depth_format(), ); -let push_constants_size = std::mem::size_of::() as u32; +let immediate_data_size = std::mem::size_of::() as u32; let pipeline = RenderPipelineBuilder::new() .with_culling(CullingMode::Back) .with_depth() - .with_push_constant(PipelineStage::VERTEX, push_constants_size) + .with_immediate_data(immediate_data_size) .with_buffer( BufferBuilder::build_from_mesh(&mesh, render_context.gpu()) .expect("Failed to create vertex buffer"), @@ -434,28 +436,28 @@ let mvp = projection.multiply(&view).multiply(&model); This multiplication order produces clip‑space positions as `mvp * vec4(position, 1)`. The final upload transposes matrices to match GLSL column‑major layout. -### Step 9 — Record Draw Commands with Push Constants +### Step 9 — Record Draw Commands with Immediates -Define a push constant struct and a helper to reinterpret it as `[u32]`. Record commands to begin the pass, set pipeline state, bind the texture and sampler, push constants, and draw 36 vertices. +Define an immediate data struct and a helper to reinterpret it as `[u32]`. Record commands to begin the pass, set pipeline state, bind the texture and sampler, set immediates, and draw 36 vertices. ```rust #[repr(C)] #[derive(Clone, Copy)] -pub struct PushConstant { +pub struct ImmediateData { mvp: [[f32; 4]; 4], model: [[f32; 4]; 4], } -pub fn push_constants_to_bytes(push_constants: &PushConstant) -> &[u32] { +pub fn immediate_data_to_bytes(immediate_data: &ImmediateData) -> &[u32] { unsafe { - let size_in_bytes = std::mem::size_of::(); + let size_in_bytes = std::mem::size_of::(); let size_in_u32 = size_in_bytes / std::mem::size_of::(); - let ptr = push_constants as *const PushConstant as *const u32; + let ptr = immediate_data as *const ImmediateData as *const u32; return std::slice::from_raw_parts(ptr, size_in_u32); } } -use lambda::render::{command::RenderCommand, pipeline::PipelineStage, viewport::ViewportBuilder}; +use lambda::render::{command::RenderCommand, viewport::ViewportBuilder}; let viewport = ViewportBuilder::new().build(self.width, self.height); let pipeline = self.render_pipeline.expect("pipeline not set"); @@ -472,11 +474,10 @@ let commands = vec![ RenderCommand::SetScissors { start_at: 0, viewports: vec![viewport.clone()] }, RenderCommand::SetBindGroup { set: 0, group, dynamic_offsets: vec![] }, RenderCommand::BindVertexBuffer { pipeline, buffer: 0 }, - RenderCommand::PushConstants { + RenderCommand::Immediates { pipeline, - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_bytes(&PushConstant { + bytes: Vec::from(immediate_data_to_bytes(&ImmediateData { mvp: mvp.transpose(), model: model.transpose(), })), @@ -510,7 +511,7 @@ fn on_event(&mut self, event: Events) -> Result { ## Notes -- Push constant limits: total size MUST be within the device’s push constant limit. This example uses 128 bytes, which fits common defaults. +- Immediate data limits: total size MUST be within the device's immediate data limit. This example uses 128 bytes, which fits common defaults. wgpu v28 requires `Features::IMMEDIATES` to be enabled. - Matrix layout: GLSL multiplies column‑major by default; transposing on upload aligns memory layout and multiplication order. - Normal transform: `mat3(model)` is correct when the model matrix contains only rotations and uniform scale. For non‑uniform scale, compute the normal matrix as the inverse‑transpose of the upper‑left 3×3. - Texture color space: use `Rgba8UnormSrgb` for color images so sampling returns linear values. @@ -520,12 +521,12 @@ fn on_event(&mut self, event: Events) -> Result { ## Conclusion This tutorial delivered a rotating, textured cube with depth testing and -back‑face culling. It compiled shaders that use a vertex push‑constant block +back‑face culling. It compiled shaders that use a vertex immediate data block for model‑view‑projection and model matrices, built a cube mesh and vertex layout, created an sRGB texture and sampler, and constructed a pipeline with -depth and culling. Per‑frame transforms were computed and uploaded via push -constants, and draw commands were recorded. The result demonstrates push -constants for per‑draw transforms alongside 2D sampling in a 3D render path. +depth and culling. Per‑frame transforms were computed and uploaded via immediates, +and draw commands were recorded. The result demonstrates immediates for per‑draw +transforms alongside 2D sampling in a 3D render path. ## Putting It Together @@ -551,6 +552,8 @@ constants for per‑draw transforms alongside 2D sampling in a 3D render path. ## Changelog +- 0.3.1 (2026-01-07): Remove stage usage from immediates API examples. +- 0.3.0 (2026-01-05): Migrate from push constants to immediates for wgpu v28; update all code examples and terminology. - 0.2.0 (2025-12-15): Update builder API calls to use `render_context.gpu()` and add `surface_format`/`depth_format` parameters to `RenderPassBuilder` and `RenderPipelineBuilder`. - 0.1.1 (2025-11-10): Add Conclusion section summarizing outcomes; update metadata and commit. -- 0.1.0 (2025-11-10): Initial draft aligned with `crates/lambda-rs/examples/textured_cube.rs` including push constants, depth, culling, and projected UV sampling. +- 0.1.0 (2025-11-10): Initial draft aligned with `crates/lambda-rs/examples/textured_cube.rs` including immediates, depth, culling, and projected UV sampling. diff --git a/docs/tutorials/uniform-buffers.md b/docs/tutorials/uniform-buffers.md index b04034f9..c180892c 100644 --- a/docs/tutorials/uniform-buffers.md +++ b/docs/tutorials/uniform-buffers.md @@ -427,8 +427,8 @@ multiple objects and passes. - Extend shaders to compute diffuse lighting. - Provide a lighting UBO at binding 2 with light position and color. -- Exercise 5: Push constants comparison - - Port to push constants (see `crates/lambda-rs/examples/push_constants.rs`) +- Exercise 5: Immediates comparison + - Port to immediates (see `crates/lambda-rs/examples/immediates.rs`) and compare trade‑offs. - Exercise 6: Per‑material uniforms diff --git a/tools/obj_loader/src/main.rs b/tools/obj_loader/src/main.rs index 8c154d9f..e02dcd0d 100644 --- a/tools/obj_loader/src/main.rs +++ b/tools/obj_loader/src/main.rs @@ -14,10 +14,6 @@ use lambda::{ WindowEvent, }, logging, - math::matrix::{ - self, - Matrix, - }, render::{ buffer::BufferBuilder, command::RenderCommand, @@ -25,10 +21,7 @@ use lambda::{ Mesh, MeshBuilder, }, - pipeline::{ - PipelineStage, - RenderPipelineBuilder, - }, + pipeline::RenderPipelineBuilder, render_pass::RenderPassBuilder, shader::{ Shader, @@ -86,20 +79,20 @@ void main() { "#; -// ------------------------------ PUSH CONSTANTS ------------------------------- +// -------------------------------- IMMEDIATES --------------------------------- #[repr(C)] #[derive(Debug, Clone, Copy)] -pub struct PushConstant { +pub struct ImmediateData { data: [f32; 4], render_matrix: [[f32; 4]; 4], } -pub fn push_constants_to_bytes(push_constants: &PushConstant) -> &[u32] { +pub fn immediate_data_to_bytes(immediate_data: &ImmediateData) -> &[u32] { let bytes = unsafe { - let size_in_bytes = std::mem::size_of::(); + let size_in_bytes = std::mem::size_of::(); let size_in_u32 = size_in_bytes / std::mem::size_of::(); - let ptr = push_constants as *const PushConstant as *const u32; + let ptr = immediate_data as *const ImmediateData as *const u32; std::slice::from_raw_parts(ptr, size_in_u32) }; @@ -174,7 +167,10 @@ struct ObjLoader { impl Component for ObjLoader { fn on_event(&mut self, event: Events) -> Result { match event { - lambda::events::Events::Window { event, issued_at } => match event { + lambda::events::Events::Window { + event, + issued_at: _, + } => match event { WindowEvent::Resize { width, height } => { self.width = width; self.height = height; @@ -197,7 +193,7 @@ impl Component for ObjLoader { let render_pass = RenderPassBuilder::new().build(gpu, surface_format, depth_format); - let push_constant_size = std::mem::size_of::() as u32; + let immediate_data_size = std::mem::size_of::() as u32; let mesh = MeshBuilder::new().build_from_obj(&self.obj_path); @@ -208,7 +204,7 @@ impl Component for ObjLoader { ); let pipeline = RenderPipelineBuilder::new() - .with_push_constant(PipelineStage::VERTEX, push_constant_size) + .with_immediate_data(immediate_data_size) .with_buffer( BufferBuilder::build_from_mesh(&mesh, gpu) .expect("Failed to create buffer"), @@ -231,14 +227,14 @@ impl Component for ObjLoader { fn on_detach( &mut self, - render_context: &mut lambda::render::RenderContext, + _render_context: &mut lambda::render::RenderContext, ) -> Result { return Ok(ComponentResult::Success); } fn on_update( &mut self, - last_frame: &std::time::Duration, + _last_frame: &std::time::Duration, ) -> Result { self.frame_number += 1; return Ok(ComponentResult::Success); @@ -246,24 +242,8 @@ impl Component for ObjLoader { fn on_render( &mut self, - render_context: &mut lambda::render::RenderContext, + _render_context: &mut lambda::render::RenderContext, ) -> Vec { - let camera = [0.0, 0.0, -2.0]; - let view: [[f32; 4]; 4] = matrix::translation_matrix(camera); - - // Create a projection matrix. - let projection: [[f32; 4]; 4] = - matrix::perspective_matrix(0.12, (4 / 3) as f32, 0.1, 200.0); - - // Rotate model. - let model: [[f32; 4]; 4] = matrix::rotate_matrix( - matrix::identity_matrix(4, 4), - [0.0, 1.0, 0.0], - 0.001 * self.frame_number as f32, - ); - - // Create render matrix. - let mesh_matrix = projection.multiply(&view).multiply(&model); let mesh_matrix = make_transform([0.0, 0.0, 0.5], self.frame_number as f32 * 0.01, 0.5); @@ -298,11 +278,10 @@ impl Component for ObjLoader { pipeline: render_pipeline.clone(), buffer: 0, }, - RenderCommand::PushConstants { + RenderCommand::Immediates { pipeline: render_pipeline.clone(), - stage: PipelineStage::VERTEX, offset: 0, - bytes: Vec::from(push_constants_to_bytes(&PushConstant { + bytes: Vec::from(immediate_data_to_bytes(&ImmediateData { data: [0.0, 0.0, 0.0, 0.0], render_matrix: mesh_matrix, })),