From 6642ff9aff39ed94edc6360a9bbf343b0b482031 Mon Sep 17 00:00:00 2001 From: Ben Hansen Date: Mon, 21 Oct 2019 18:12:28 -0600 Subject: [PATCH] tutorial3-pipeline --- Cargo.lock | 182 ++++++++ FEATURES.md | 8 +- code/Cargo.toml | 5 + code/src/beginner/tutorial3-pipeline/main.rs | 215 +++++++++ .../beginner/tutorial3-pipeline/shader.frag | 7 + .../beginner/tutorial3-pipeline/shader.vert | 11 + docs/.vuepress/config.js | 2 + docs/beginner/tutorial2-swapchain.md | 2 +- .../beginner/tutorial3-pipeline-composite.svg | 415 ++++++++++++++++++ docs/beginner/tutorial3-pipeline-triangle.png | Bin 0 -> 7411 bytes docs/beginner/tutorial3-pipeline-vertices.png | Bin 0 -> 26327 bytes docs/beginner/tutorial3-pipeline-vertices.svg | 199 +++++++++ docs/beginner/tutorial3-pipeline.md | 261 +++++++++++ 13 files changed, 1305 insertions(+), 2 deletions(-) create mode 100644 code/src/beginner/tutorial3-pipeline/main.rs create mode 100644 code/src/beginner/tutorial3-pipeline/shader.frag create mode 100644 code/src/beginner/tutorial3-pipeline/shader.vert create mode 100644 docs/beginner/tutorial3-pipeline-composite.svg create mode 100644 docs/beginner/tutorial3-pipeline-triangle.png create mode 100644 docs/beginner/tutorial3-pipeline-vertices.png create mode 100644 docs/beginner/tutorial3-pipeline-vertices.svg create mode 100644 docs/beginner/tutorial3-pipeline.md diff --git a/Cargo.lock b/Cargo.lock index 99d276b3..10bdf0b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,11 @@ dependencies = [ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "arrayref" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "arrayvec" version = "0.4.12" @@ -92,11 +97,34 @@ name = "block" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "block-buffer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "byte-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "byteorder" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "c2-chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "calloop" version = "0.4.4" @@ -125,6 +153,14 @@ dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "cmake" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cocoa" version = "0.18.4" @@ -143,6 +179,7 @@ dependencies = [ name = "code" version = "0.1.0" dependencies = [ + "glsl-to-spirv 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.22.3 (registry+https://github.com/rust-lang/crates.io-index)", "raw-window-handle 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "wgpu 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -267,6 +304,14 @@ dependencies = [ "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "digest" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "dispatch" version = "0.1.4" @@ -310,6 +355,11 @@ dependencies = [ "synstructure 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "foreign-types" version = "0.3.2" @@ -345,6 +395,24 @@ dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "generic-array" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "getrandom" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "gfx-backend-empty" version = "0.3.1" @@ -392,6 +460,16 @@ dependencies = [ "lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "glsl-to-spirv" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cmake 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "hibitset" version = "0.6.2" @@ -715,6 +793,11 @@ dependencies = [ "inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ppv-lite86" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "proc-macro2" version = "0.4.30" @@ -747,6 +830,43 @@ dependencies = [ "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_chacha" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "raw-window-handle" version = "0.1.2" @@ -792,6 +912,14 @@ dependencies = [ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "remove_dir_all" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rendy-descriptor" version = "0.4.1" @@ -884,6 +1012,17 @@ name = "semver-parser" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "sha2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "shared_library" version = "0.1.9" @@ -968,6 +1107,19 @@ dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tiff" version = "0.3.1" @@ -979,6 +1131,11 @@ dependencies = [ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "typenum" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-xid" version = "0.1.0" @@ -1009,6 +1166,11 @@ dependencies = [ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "wasi" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "wayland-client" version = "0.23.6" @@ -1222,6 +1384,7 @@ dependencies = [ "checksum andrew 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9b7f09f89872c2b6b29e319377b1fbe91c6f5947df19a25596e121cf19a7b35e" "checksum android_glue 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" "checksum approx 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +"checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" "checksum arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" "checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" "checksum ash 0.29.0 (registry+https://github.com/rust-lang/crates.io-index)" = "003d1fb2eb12eb06d4a03dbe02eea67a9fac910fa97932ab9e3a75b96a1ea5e5" @@ -1231,11 +1394,15 @@ dependencies = [ "checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +"checksum block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab" +"checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" +"checksum c2-chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7d64d04786e0f528460fc884753cf8dddcc466be308f6026f8e355c41a0e4101" "checksum calloop 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7aa2097be53a00de9e8fc349fea6d76221f398f5c4fa550d420669906962d160" "checksum cc 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)" = "4fc9a35e1f4290eb9e5fc54ba6cf40671ed2a2514c3eeb2b2a908dda2ea5a1be" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum cmake 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "81fb25b677f8bf1eb325017cb6bb8452f87969db0fedb4f757b297bee78a7c62" "checksum cocoa 0.18.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cf79daa4e11e5def06e55306aa3601b87de6b5149671529318da048f67cdd77b" "checksum color_quant 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" "checksum colorful 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0bca1619ff57dd7a56b58a8e25ef4199f123e78e503fe1653410350a1b98ae65" @@ -1251,21 +1418,26 @@ dependencies = [ "checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" "checksum deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" "checksum derivative 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "942ca430eef7a3806595a6737bc388bf51adb888d3fc0dd1b50f1c170167ee3a" +"checksum digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "03b072242a8cbaf9c145665af9d250c59af3b958f83ed6824e13533cf76d5b90" "checksum dispatch 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e93ca78226c51902d7aa8c12c988338aadd9e85ed9c6be8aaac39192ff3605" "checksum dlib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "77e51249a9d823a4cb79e3eca6dcd756153e8ed0157b6c04775d04bf1b13b76a" "checksum downcast-rs 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5fe414cc2fd4447b7da94b27ddfb6831a8a06f35f6d077ab5613ec703866c49a" "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" "checksum failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9" "checksum failure_derive 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" "checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +"checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" +"checksum getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571" "checksum gfx-backend-empty 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf40d1cd04f92f8555db32dd9fb5bb13d17ae4624318b73604b4186b1d66f0a" "checksum gfx-backend-vulkan 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5facf0b6e4306a6a93682077f85602c89b49aca8ecceb73b9558e7baa37f8c54" "checksum gfx-hal 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00a079b66250a6437c3822c8bfc25e5bed5ba0186d312e7df87bc503325ab33c" "checksum gif 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" +"checksum glsl-to-spirv 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "28caebc98746d507603a2d3df66dcbe04e41d4febad0320f3eec1ef72b6bbef1" "checksum hibitset 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "47e7292fd9f7fe89fa35c98048f2d0a69b79ed243604234d18f6f8a1aa6f408d" "checksum image 0.22.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4be8aaefbe7545dc42ae925afb55a0098f226a3fe5ef721872806f44f57826" "checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" @@ -1303,15 +1475,21 @@ dependencies = [ "checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" "checksum pkg-config 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "72d5370d90f49f70bd033c3d75e87fc529fbfff9d6f7cccef07d6170079d91ea" "checksum png 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8422b27bb2c013dd97b9aef69e161ce262236f49aaf46a0489011c8ff0264602" +"checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90cf5f418035b98e655e9cdb225047638296b862b42411c4e45bb88d700f7fc0" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" +"checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" +"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" "checksum raw-window-handle 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "af3d3b2e1053b3ff2171efc29a8bff3439ce6b2ce6a0432695134bc1c7ff8e87" "checksum rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "83a27732a533a1be0a0035a111fe76db89ad312f6f0347004c220c57f209a123" "checksum rayon-core 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98dcf634205083b17d0861252431eb2acbfb698ab7478a2d20de07954f47ec7b" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum relevant 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bbc232e13d37f4547f5b9b42a5efc380cabe5dbc1807f8b893580640b2ab0308" +"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" "checksum rendy-descriptor 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ca7bcc3cb86a7945ecc5f0d7121e47a0b5979c3c57d3a5e6facc8738338651d8" "checksum rendy-memory 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1cf6b3fc8a012b69062419caf086d35f83d9af57bf30a6971691731b4816a47f" "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" @@ -1323,6 +1501,7 @@ dependencies = [ "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +"checksum sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0" "checksum shared_library 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7" @@ -1332,12 +1511,15 @@ dependencies = [ "checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf" "checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" "checksum synstructure 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f085a5855930c0441ca1288cf044ea4aecf4f43a91668abdb870b4ba546a203" +"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum tiff 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d7b7c2cfc4742bd8a32f2e614339dd8ce30dbcf676bb262bd63a2327bc5df57d" +"checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e" +"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" "checksum wayland-client 0.23.6 (registry+https://github.com/rust-lang/crates.io-index)" = "af1080ebe0efabcf12aef2132152f616038f2d7dcbbccf7b2d8c5270fe14bcda" "checksum wayland-commons 0.23.6 (registry+https://github.com/rust-lang/crates.io-index)" = "bb66b0d1a27c39bbce712b6372131c6e25149f03ffb0cd017cf8f7de8d66dbdb" "checksum wayland-protocols 0.23.6 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc286643656742777d55dc8e70d144fa4699e426ca8e9d4ef454f4bf15ffcf9" diff --git a/FEATURES.md b/FEATURES.md index 844ef5ac..2bab7497 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,4 +1,10 @@ # Features ## Planned -* Framebuffer \ No newline at end of file +* Framebuffer +* code download links + +## Things I'd like +* Screen shots +* Displaying random models as wireframe and solid +* loading obj models \ No newline at end of file diff --git a/code/Cargo.toml b/code/Cargo.toml index 8dee808a..44ac288d 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -14,10 +14,15 @@ path = "src/beginner/tutorial1-window.rs" name = "tutorial2-swapchain" path = "src/beginner/tutorial2-swapchain.rs" +[[bin]] +name = "tutorial3-pipeline" +path = "src/beginner/tutorial3-pipeline/main.rs" + [dependencies] image = "0.22" raw-window-handle = "0.1" winit = "0.20.0-alpha3" +glsl-to-spirv = "0.1" [dependencies.wgpu] version = "0.3" diff --git a/code/src/beginner/tutorial3-pipeline/main.rs b/code/src/beginner/tutorial3-pipeline/main.rs new file mode 100644 index 00000000..fe86b576 --- /dev/null +++ b/code/src/beginner/tutorial3-pipeline/main.rs @@ -0,0 +1,215 @@ +use winit::{ + event::*, + event_loop::{EventLoop, ControlFlow}, + window::{Window, WindowBuilder}, +}; + +struct State { + surface: wgpu::Surface, + device: wgpu::Device, + sc_desc: wgpu::SwapChainDescriptor, + swap_chain: wgpu::SwapChain, + + render_pipeline: wgpu::RenderPipeline, + + hidpi_factor: f64, + size: winit::dpi::LogicalSize, +} + +impl State { + fn new(window: &Window) -> Self { + let hidpi_factor = window.hidpi_factor(); + let size = window.inner_size(); + let physical_size = size.to_physical(hidpi_factor); + + let instance = wgpu::Instance::new(); + + use raw_window_handle::HasRawWindowHandle as _; + let surface = instance.create_surface(window.raw_window_handle()); + + let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: Default::default(), + }); + + let device = adapter.request_device(&wgpu::DeviceDescriptor { + extensions: wgpu::Extensions { + anisotropic_filtering: false, + }, + limits: Default::default(), + }); + + let sc_desc = wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: physical_size.width.round() as u32, + height: physical_size.height.round() as u32, + present_mode: wgpu::PresentMode::Vsync, + }; + let swap_chain = device.create_swap_chain(&surface, &sc_desc); + + let vs_src = include_str!("shader.vert"); + let fs_src = include_str!("shader.frag"); + let vs_spirv = glsl_to_spirv::compile(vs_src, glsl_to_spirv::ShaderType::Vertex).unwrap(); + let fs_spirv = glsl_to_spirv::compile(fs_src, glsl_to_spirv::ShaderType::Fragment).unwrap(); + let vs_data = wgpu::read_spirv(vs_spirv).unwrap(); + let fs_data = wgpu::read_spirv(fs_spirv).unwrap(); + let vs_module = device.create_shader_module(&vs_data); + let fs_module = device.create_shader_module(&fs_data); + + let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &render_pipeline_layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fs_module, + entry_point: "main", + }), + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Ccw, + cull_mode: wgpu::CullMode::Back, + depth_bias: 0, + depth_bias_slope_scale: 0.0, + depth_bias_clamp: 0.0, + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[ + wgpu::ColorStateDescriptor { + format: sc_desc.format, + color_blend: wgpu::BlendDescriptor::REPLACE, + alpha_blend: wgpu::BlendDescriptor::REPLACE, + write_mask: wgpu::ColorWrite::ALL, + }, + ], + depth_stencil_state: None, + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[], + sample_count: 1, + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + Self { + surface, + device, + sc_desc, + swap_chain, + render_pipeline, + hidpi_factor, + size, + } + } + + fn update_hidpi_and_resize(&mut self, new_hidpi_factor: f64) { + self.hidpi_factor = new_hidpi_factor; + self.resize(self.size); + } + + fn resize(&mut self, new_size: winit::dpi::LogicalSize) { + let physical_size = new_size.to_physical(self.hidpi_factor); + self.size = new_size; + self.sc_desc.width = physical_size.width.round() as u32; + self.sc_desc.height = physical_size.height.round() as u32; + self.swap_chain = self.device.create_swap_chain(&self.surface, &self.sc_desc); + } + + fn input(&mut self, event: &WindowEvent) -> bool { + false + } + + fn update(&mut self) { + + } + + fn render(&mut self) { + let frame = self.swap_chain.get_next_texture(); + + let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + todo: 0, + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment: &frame.view, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 0.1, + g: 0.2, + b: 0.3, + a: 1.0, + }, + } + ], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.render_pipeline); + render_pass.draw(0..3, 0..1); + } + + self.device.get_queue().submit(&[ + encoder.finish() + ]); + } +} + +fn main() { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .build(&event_loop) + .unwrap(); + + let mut state = State::new(&window); + + event_loop.run(move |event, _, control_flow| { + match event { + Event::WindowEvent { + ref event, + window_id, + } if window_id == window.id() => if state.input(event) { + *control_flow = ControlFlow::Wait; + } else { + match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::KeyboardInput { + input, + .. + } => { + match input { + KeyboardInput { + state: ElementState::Pressed, + virtual_keycode: Some(VirtualKeyCode::Escape), + .. + } => *control_flow = ControlFlow::Exit, + _ => *control_flow = ControlFlow::Wait, + } + } + WindowEvent::Resized(logical_size) => { + state.resize(*logical_size); + *control_flow = ControlFlow::Wait; + } + WindowEvent::HiDpiFactorChanged(new_hidpi_factor) => { + state.update_hidpi_and_resize(*new_hidpi_factor); + *control_flow = ControlFlow::Wait; + } + _ => *control_flow = ControlFlow::Wait, + } + } + Event::EventsCleared => { + state.update(); + state.render(); + *control_flow = ControlFlow::Wait; + } + _ => *control_flow = ControlFlow::Wait, + } + }); +} \ No newline at end of file diff --git a/code/src/beginner/tutorial3-pipeline/shader.frag b/code/src/beginner/tutorial3-pipeline/shader.frag new file mode 100644 index 00000000..49493067 --- /dev/null +++ b/code/src/beginner/tutorial3-pipeline/shader.frag @@ -0,0 +1,7 @@ +#version 450 + +layout(location=0) out vec4 f_color; + +void main() { + f_color = vec4(0.3, 0.2, 0.1, 1.0); +} \ No newline at end of file diff --git a/code/src/beginner/tutorial3-pipeline/shader.vert b/code/src/beginner/tutorial3-pipeline/shader.vert new file mode 100644 index 00000000..ee871f69 --- /dev/null +++ b/code/src/beginner/tutorial3-pipeline/shader.vert @@ -0,0 +1,11 @@ +#version 450 + +const vec2 positions[3] = vec2[3]( + vec2(0.0, -0.5), + vec2(-0.5, 0.5), + vec2(0.5, 0.5) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); +} \ No newline at end of file diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index a75cb902..c83e5522 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -1,5 +1,6 @@ module.exports = { base: '/learn-wgpu/', + title: 'Learn Wgpu', themeConfig: { displayAllHeaders: false, lastUpdated: 'Last Updated', @@ -13,6 +14,7 @@ module.exports = { '/beginner/', '/beginner/tutorial1-window', '/beginner/tutorial2-swapchain', + '/beginner/tutorial3-pipeline', ], }, { diff --git a/docs/beginner/tutorial2-swapchain.md b/docs/beginner/tutorial2-swapchain.md index a0fcfe61..3a932f36 100644 --- a/docs/beginner/tutorial2-swapchain.md +++ b/docs/beginner/tutorial2-swapchain.md @@ -342,7 +342,7 @@ The `RenderPassColorAttachmentDescriptor` has the `attachment` field which infor There's not much documentation for `resolve_target` at the moment, but it does expect an `Option<&'a TextureView>`. Fortunately, we can use `None`. -`load_op` and `store_op` define what operation to perform when gpu looks to load and store the colors for this color attachment for this render pass. We'll get more into this when we cover alpha blending, but for now we just `LoadOp::Clear` the texture when the render pass starts, and `StoreOp::Store` the colors when it ends. +`load_op` and `store_op` define what operation to perform when gpu looks to load and store the colors for this color attachment for this render pass. We'll get more into this when we cover render passes in depth, but for now we just `LoadOp::Clear` the texture when the render pass starts, and `StoreOp::Store` the colors when it ends. The last field `clear_color` is just the color to use when `LoadOp::Clear` and/or `StoreOp::Clear` are used. This is where the blue color comes from. diff --git a/docs/beginner/tutorial3-pipeline-composite.svg b/docs/beginner/tutorial3-pipeline-composite.svg new file mode 100644 index 00000000..373c4beb --- /dev/null +++ b/docs/beginner/tutorial3-pipeline-composite.svg @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + VerticesVer V + Vertices sdgfhdcsd sdfsdf + sdfsdfsf + f + f + ssssssssdafsdfm,./xcz Vertices + + + + + + diff --git a/docs/beginner/tutorial3-pipeline-triangle.png b/docs/beginner/tutorial3-pipeline-triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..9dbe340c18bc6fa72b5bb4f40077dab591fb14b7 GIT binary patch literal 7411 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 literal 0 HcmV?d00001 diff --git a/docs/beginner/tutorial3-pipeline-vertices.png b/docs/beginner/tutorial3-pipeline-vertices.png new file mode 100644 index 0000000000000000000000000000000000000000..f241c4b0454b28543e5326bc6d861b90f1da345a GIT binary patch literal 26327 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 Pu%4^ys#QLf2d$ literal 0 HcmV?d00001 diff --git a/docs/beginner/tutorial3-pipeline-vertices.svg b/docs/beginner/tutorial3-pipeline-vertices.svg new file mode 100644 index 00000000..bc8143c4 --- /dev/null +++ b/docs/beginner/tutorial3-pipeline-vertices.svg @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + Vertices + + + + + + diff --git a/docs/beginner/tutorial3-pipeline.md b/docs/beginner/tutorial3-pipeline.md new file mode 100644 index 00000000..fbfabe70 --- /dev/null +++ b/docs/beginner/tutorial3-pipeline.md @@ -0,0 +1,261 @@ +# The Pipeline + +## What's a pipeline? +If you're familiar with OpenGL, you may remember using shader programs. You can think of a pipeline as a more robust version of that. A pipeline describes all the actions the gpu will preform when acting on a set of data. In this section, we will be creating a `RenderPipeline` specifically. + +## Wait shaders? +Shader's are mini programs that you send to the gpu to perform operations on your data. There are 3 main types of shader: vertex, fragment, and compute. There are others such as geometry shaders, but they're more of an advanced topic. For now we're just going to use vertex, and fragment shaders. + +## Vertex, fragment.. what are those? +A vertex is a point in 3d space (can also be 2d). These vertices are then bundle in groups of 2s to form lines and/or 3s to form triangles. + + + +Most modern rendering uses triangles to make simple shapes such as a cube, and complex shapes such as person. + + + +We use a vertex shader to manipulate a list of vertices, in order to transform the shape to look they way we want it. + +You can think of a fragment as the beginnings of a pixel in the resulting image. Each fragment has a color that will be copied to it's corresponding pixel. The fragment shader decides what color the fragment will be. + +## GLSL and SPIR-V +Shader's in `wgpu` are written with a binary language called [SPIR-V](https://www.khronos.org/registry/spir-v/). SPIR-V is designed for computers to read not people, so we're going to use a language called GLSL to write our code, and then convert that to SPIR-V. + +In order to do that, we're going to need something to do the conversion. Add the following crate to you're dependencies. + +```toml +[dependencies] +# ... +glsl-to-spirv = "0.1" +``` + +We'll use this in a bit, but first let's create the shaders. + +## Writing the shaders +In the same folder as `main.rs`, create two (2) files: `shader.vert`, and `shader.frag`. Write the following code in `shader.vert`. + +```glsl +// shader.vert +#version 450 + +const vec2 positions[3] = vec2[3]( + vec2(0.0, -0.5), + vec2(-0.5, 0.5), + vec2(0.5, 0.5) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); +} +``` + +If you've used C/C++ before (or even Java), this syntax should be somewhat familiar. There are some key differences though that i'll go over. + +First up there's the `#version 450` line. This specifies the version of GLSL that we're using. I've gone with a later version so we can use many of the advanced GLSL features. + +We're currently storing vertex data in the shader as `positions`. This is bad practice as it limits what we can draw with this shader, and it can make the shader super big if we want to use a complex model. Using actual vertex data requires us to use `Buffer`s, which we'll talk about next time, so we'll turn a blind eye for now. + +There's also `gl_Position` and `gl_VertexIndex` which are built in variables that define where the vertex position data is going to be saved stored as 4 floats, and the index of the current vertex in the vertex data. + +Next up `shader.frag`. + +```glsl +// shader.frag +#version 450 + +layout(location=0) out vec4 f_color; + +void main() { + f_color = vec4(0.3, 0.2, 0.1, 1.0); +} +``` + +The part that sticks out is the `layout(location=0) out vec4 f_color;` line. In GLSL you can create `in` and `out` variables in your shaders. An `in` variable will expect data from outside the shader. In the case of the vertex shader, this will come from vertex data. In a fragment shader, an `in` variable will pull from `out` variables in the vertex shader. When an `out` variable is defined in the fragment shader, it means that the value is meant to be written to a buffer to be used outside the shader program. + +`in` and `out` variables can also specify a layout. In `shader.frag` we specify that the `out vec4 f_color` should be `layout(location=0)`; this means that the value of `f_color` will be saved to whatever buffer is at location zero in our application. In most cases, `location=0` is the current texture from the swapchain aka. the screen. + +You may have noticed that `shader.vert` doesn't have any `in` variables nor `out` variables. `gl_Position` functions as an out variable for vertex position data, so `shader.vert` doesn't need any `out` variables. If we wanted to send more data to fragment shader, we could specify an `out` variable in `shader.vert` and an in variable in `shader.frag`. *Note: the location has to match, otherwise the GLSL code will fail to compile* + +```glsl +// shader.vert +layout(location=0) out vec4 v_color; + +// shader.frag +layout(location=0) in vec4 v_color; +``` + +## How do we use the shaders? +This is the part where we finally make the thing in the title: the pipeline. First let's modify `State` to include the following. + +```rust +// main.rs +struct State { + surface: wgpu::Surface, + device: wgpu::Device, + sc_desc: wgpu::SwapChainDescriptor, + swap_chain: wgpu::SwapChain, + + // NEW! + render_pipeline: wgpu::RenderPipeline, + + hidpi_factor: f64, + size: winit::dpi::LogicalSize, +} +``` + +Now let's move to the `new()` method, and start making the pipeline. We'll have to load in those shaders we made earlier, as the `render_pipeline` requires those. + +```rust +let vs_src = include_str!("shader.vert"); +let fs_src = include_str!("shader.frag"); + +let vs_spirv = glsl_to_spirv::compile(vs_src, glsl_to_spirv::ShaderType::Vertex).unwrap(); +let fs_spirv = glsl_to_spirv::compile(fs_src, glsl_to_spirv::ShaderType::Fragment).unwrap(); + +let vs_data = wgpu::read_spirv(vs_spirv).unwrap(); +let fs_data = wgpu::read_spirv(fs_spirv).unwrap(); + +let vs_module = device.create_shader_module(&vs_data); +let fs_module = device.create_shader_module(&fs_data); +``` + +One more thing, we need to create a `PipelineLayout`. We'll get more into this after we cover `Buffer`s. + +```rust +let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[], +}); +``` + +Finally we have all we need to create the `render_pipeline`. + +```rust +let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &render_pipeline_layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", // 1. + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { // 2. + module: &fs_module, + entry_point: "main", + }), +``` + +Two things to note here: +1. You can specify an `entry_point` for your shaders. I normally use `"main"` as that's what it would be in OpenGL, but feel free to use whatever name you like. +2. The `fragment_stage` is technically optional, so you have to wrap it in `Some()`. I've never not used a fragment shader with a vertex shader, but the option is available if you need it. + +```rust + // continued ... + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Ccw, + cull_mode: wgpu::CullMode::Back, + depth_bias: 0, + depth_bias_slope_scale: 0.0, + depth_bias_clamp: 0.0, + }), +``` + +`rasterization_state` describes how to process primitives (in our case triangles) before they are sent to the fragment shader (or the next stage in the pipeline if there is none). Primitives that don't meet the criteria are *culled* (aka. not rendered). Culling helps speed up the rendering process by not rendering things that should be visible anyway. + +We'll cover culling a bit more when we cover `Buffer`s. + +```rust + // continued ... + color_states: &[ + wgpu::ColorStateDescriptor { + format: sc_desc.format, + color_blend: wgpu::BlendDescriptor::REPLACE, + alpha_blend: wgpu::BlendDescriptor::REPLACE, + write_mask: wgpu::ColorWrite::ALL, + }, + ], +``` +A `color_state` describes how colors are stored and processed throughout the pipeline. You can have multiple color states, but we only need one as we're just drawing to the screen. We use the `swap_chain`'s format so that copying to it is easy, and we specify that the blending should just replace old pixel data with new data. We also tell `wgpu` to write to all colors: red, blue, green, and alpha. *We'll talk more about*`color_state` *when we talk about textures.* + +```rust + // continued ... + primitive_topology: wgpu::PrimitiveTopology::TriangleList, // 1. + depth_stencil_state: None, // 2. + index_format: wgpu::IndexFormat::Uint16, // 3. + vertex_buffers: &[], // 4. + sample_count: 1, // 5. + sample_mask: !0, // 6. + alpha_to_coverage_enabled: false, // 7. +}); +``` + +The rest of the method is pretty simple: +1. We tell `wgpu` that we want to use a list of triangles for drawing. +2. We're not using a depth/stencil buffer currently, so we leave `depth_stencil_state` as `None`. *This will change later*. +3. We specify the type of index we want to use. In this case a 16-bit unsigned integer. We'll talk about indices when we talk about `Buffer`s +4. `vertex_buffers` is a pretty big topic, and as you might have guessed, we'll talk about it when we talk about buffers. +5. This determines how many samples this pipeline will use. Multisampling is a complex topic, so we won't get into it here. +6. `sample_mask` specifies which samples should be active. In this case we are using all of them. +7. `alpha_to_coverage_enabled` has to do with anti-aliasing. We're not covering anti-aliasing here, so we'll leave this as false now. + + + +Now all we have to do is save the `render_pipeline` to `State` and then we can use it! + +```rust +// new() +Self { + surface, + device, + sc_desc, + swap_chain, + // NEW! + render_pipeline, + hidpi_factor, + size, +} +``` +## Using a pipeline + +If you run you're program now, it'll take a little longer to start, but it will still show the blue screen we got in the last section. That's because while we created the `render_pipeline`. We need to modify the code in `render()` to actually use it. + +```rust +// render() + +// ... +{ + // 1. + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment: &frame.view, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 0.1, + g: 0.2, + b: 0.3, + a: 1.0, + }, + } + ], + depth_stencil_attachment: None, + }); + + // NEW! + render_pass.set_pipeline(&self.render_pipeline); // 2. + render_pass.draw(0..3, 0..1); // 3. +} +// ... +``` + +We didn't change much, but let's talk about what we did change. +1. We renamed `_render_pass` to `render_pass` and made it mutable. +2. We set the pipeline on the `render_pass` using the one we just created. +3. We tell `wgpu` to draw *something* with 3 vertices, and 1 instance. This is where `gl_VertexIndex` comes from. + +With all that you should be seeing a lovely brown triangle. + +![Said lovely brown triangle](./tutorial3-pipeline-triangle.png) + +## Challenge +Create a second pipeline that uses the triangles position data to create a color that it then sends to the fragment shader to use for `f_color`. Have the app swap between these when you press the spacebar. *Hint: use*`in`*and*`out`*variables.* \ No newline at end of file