diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4922c6c..de55889 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,8 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: 1.86 + - name: Install development libraries on ubuntu + run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev - run: cargo build --workspace build-features-default: @@ -38,6 +40,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: Install development libraries on ubuntu + run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev - run: cargo build --workspace fmt: @@ -126,6 +130,10 @@ jobs: cache-all-crates: "true" save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Install development libraries on ubuntu + if: ${{ matrix.platform.os == 'ubuntu-latest' }} + run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev + - name: Setup run: ${{ matrix.platform.setup }} shell: bash diff --git a/Cargo.lock b/Cargo.lock index a05f0fa..9a9c200 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -88,6 +97,30 @@ dependencies = [ "raw-window-handle", ] +[[package]] +name = "anyrender_skia" +version = "0.1.0" +dependencies = [ + "anyrender", + "ash 0.38.0+1.3.281", + "ash-window", + "debug_timer", + "futures-intrusive", + "gl", + "glutin", + "kurbo 0.12.0", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "peniko", + "pollster 0.4.0", + "raw-window-handle", + "rustc-hash 1.1.0", + "skia-safe", +] + [[package]] name = "anyrender_svg" version = "0.6.3" @@ -111,7 +144,7 @@ dependencies = [ "peniko", "pollster 0.4.0", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "vello", "wgpu 26.0.1", "wgpu_context", @@ -141,7 +174,7 @@ dependencies = [ "peniko", "pollster 0.4.0", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "vello_common", "vello_hybrid", "wgpu 26.0.1", @@ -184,6 +217,17 @@ dependencies = [ "libloading 0.8.9", ] +[[package]] +name = "ash-window" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82" +dependencies = [ + "ash 0.38.0+1.3.281", + "raw-window-handle", + "raw-window-metal", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -202,6 +246,26 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.9.4", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.106", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -259,7 +323,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", ] [[package]] @@ -360,6 +424,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.3" @@ -378,6 +451,56 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -637,6 +760,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", +] + [[package]] name = "dlib" version = "0.5.2" @@ -761,6 +894,18 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.3" @@ -897,6 +1042,15 @@ dependencies = [ "weezl", ] +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -908,6 +1062,12 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "glow" version = "0.13.1" @@ -932,6 +1092,51 @@ dependencies = [ "web-sys", ] +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.9.4", + "cfg_aliases 0.2.1", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys 0.6.1", + "libloading 0.8.9", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + [[package]] name = "glutin_wgl_sys" version = "0.5.0" @@ -1097,6 +1302,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1153,6 +1364,21 @@ dependencies = [ "hashbrown 0.16.0", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jni" version = "0.21.1" @@ -1376,6 +1602,12 @@ dependencies = [ "paste", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1409,7 +1641,7 @@ dependencies = [ "indexmap", "log", "num-traits", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "termcolor", "thiserror 1.0.69", @@ -1436,7 +1668,7 @@ dependencies = [ "log", "num-traits", "once_cell", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "thiserror 2.0.17", "unicode-ident", @@ -1481,6 +1713,16 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1539,6 +1781,15 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -1548,11 +1799,24 @@ dependencies = [ "bitflags 2.9.4", "block2", "libc", - "objc2", + "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", ] [[package]] @@ -1563,9 +1827,9 @@ checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.9.4", "block2", - "objc2", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -1575,8 +1839,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -1587,8 +1851,19 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.9.4", "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.4", + "dispatch2", + "objc2 0.6.3", ] [[package]] @@ -1598,9 +1873,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", ] [[package]] @@ -1610,9 +1885,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ "block2", - "objc2", + "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -1631,7 +1906,18 @@ dependencies = [ "block2", "dispatch", "libc", - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-foundation", ] [[package]] @@ -1641,9 +1927,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -1654,8 +1940,19 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.9.4", "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-foundation 0.3.2", ] [[package]] @@ -1666,9 +1963,22 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.9.4", "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", ] [[package]] @@ -1677,8 +1987,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -1689,14 +1999,14 @@ checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.9.4", "block2", - "objc2", + "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -1709,8 +2019,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -1721,9 +2031,9 @@ checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.9.4", "block2", - "objc2", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -1959,6 +2269,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.106", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2034,6 +2354,18 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "raw-window-metal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1" +dependencies = [ + "cocoa", + "core-graphics 0.23.2", + "objc", + "raw-window-handle", +] + [[package]] name = "rayon" version = "1.11.0" @@ -2082,6 +2414,35 @@ dependencies = [ "bitflags 2.9.4", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -2100,6 +2461,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.44" @@ -2150,6 +2517,12 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "safe_arch" version = "0.7.4" @@ -2223,6 +2596,28 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2250,6 +2645,33 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skia-bindings" +version = "0.89.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f06b9b01a0d1189fa756bb9aec034554d52ad53f2f33d80a96e7a4ecfbd3d989" +dependencies = [ + "bindgen", + "cc", + "flate2", + "heck", + "pkg-config", + "regex", + "serde_json", + "tar", + "toml", +] + +[[package]] +name = "skia-safe" +version = "0.89.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced1fcc0fa7510f9339988f31d831e14f64f9aa771e5f93f4935431e6fe69d4c" +dependencies = [ + "bitflags 2.9.4", + "skia-bindings", +] + [[package]] name = "skrifa" version = "0.37.0" @@ -2331,9 +2753,9 @@ dependencies = [ "js-sys", "log", "memmap2", - "objc2", - "objc2-foundation", - "objc2-quartz-core", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", "raw-window-handle", "redox_syscall 0.5.18", "rustix 0.38.44", @@ -2418,6 +2840,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2529,11 +2962,26 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -2552,13 +3000,19 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tracing" version = "0.1.41" @@ -3096,7 +3550,7 @@ dependencies = [ "parking_lot", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 1.0.69", "web-sys", @@ -3125,7 +3579,7 @@ dependencies = [ "portable-atomic", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.17", "wgpu-core-deps-apple", @@ -3198,7 +3652,7 @@ dependencies = [ "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 1.0.69", "wasm-bindgen", @@ -3451,6 +3905,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.4", +] + [[package]] name = "windows-sys" version = "0.61.1" @@ -3484,13 +3947,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3503,6 +3983,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3515,6 +4001,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3527,12 +4019,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3545,6 +4049,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3557,6 +4067,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3569,6 +4085,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3581,6 +4103,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winit" version = "0.30.12" @@ -3604,9 +4132,9 @@ dependencies = [ "libc", "memmap2", "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", @@ -3638,6 +4166,7 @@ name = "winit-example" version = "0.1.0" dependencies = [ "anyrender", + "anyrender_skia", "anyrender_vello", "anyrender_vello_cpu", "anyrender_vello_hybrid", @@ -3694,6 +4223,16 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.2", +] + [[package]] name = "xcursor" version = "0.3.10" diff --git a/Cargo.toml b/Cargo.toml index 2fa9a87..51aa555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/anyrender", + "crates/anyrender_skia", "crates/anyrender_vello", "crates/anyrender_vello_cpu", "crates/anyrender_vello_hybrid", @@ -25,6 +26,7 @@ rust-version = "1.86.0" [workspace.dependencies] # AnyRender dependencies (in-repo) anyrender = { version = "0.6.0", path = "./crates/anyrender" } +anyrender_skia = { version = "0.1.0", path = "./crates/anyrender_skia" } anyrender_vello = { version = "0.6.0", path = "./crates/anyrender_vello" } anyrender_vello_cpu = { version = "0.8.0", path = "./crates/anyrender_vello_cpu" } anyrender_vello_hybrid = { version = "0.1.0", path = "./crates/anyrender_vello_hybrid" } @@ -56,9 +58,9 @@ image = { version = "0.25", default-features = false } # Other dependencies debug_timer = "0.1.1" rustc-hash = "1.1.0" -futures-util = "0.3.30" +futures-util = "0.3.31" futures-intrusive = "0.5.0" pollster = "0.4" # Dev-dependencies -winit = { version = "0.30.2", features = ["rwh_06"] } \ No newline at end of file +winit = { version = "0.30.2", features = ["rwh_06"] } diff --git a/crates/anyrender_skia/Cargo.toml b/crates/anyrender_skia/Cargo.toml new file mode 100644 index 0000000..65b9970 --- /dev/null +++ b/crates/anyrender_skia/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "anyrender_skia" +description = "Skia backend for anyrender" +version = "0.1.0" +documentation = "https://docs.rs/anyrender_skia" +homepage.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true + +[features] +log_frame_times = ["debug_timer/enable"] +vulkan = ["skia-safe/vulkan"] + +[dependencies] +anyrender = { workspace = true } +debug_timer = { workspace = true } +kurbo = { workspace = true } +peniko = { workspace = true } +raw-window-handle = { workspace = true } +futures-intrusive = { workspace = true } +pollster = { workspace = true } +rustc-hash = { workspace = true } +skia-safe = { version = "0.89.1", features = ["gl", "pdf", "textlayout"]} +gl = "0.14.0" +ash = "^0.38.0" +ash-window = "0.13.0" +glutin = { version = "0.32.3", features = ["default", "wgl", "egl"] } + +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = { version = "0.6.3", default-features = false } +objc2-core-foundation = { version = "0.3.2", default-features = false } +objc2-metal = { version = "0.3.2", default-features = false, features = ["MTLCommandBuffer", "MTLCommandQueue", "MTLDrawable", "MTLPixelFormat"] } +objc2-app-kit = { version = "0.3.2", default-features = false, features = ["objc2-quartz-core", "NSView"] } +objc2-quartz-core = { version = "0.3.2", default-features = false, features = ["objc2-core-foundation", "objc2-metal", "CAMetalLayer"] } +skia-safe = { version = "0.89.1", features = ["metal"] } diff --git a/crates/anyrender_skia/src/image_renderer.rs b/crates/anyrender_skia/src/image_renderer.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/anyrender_skia/src/image_renderer.rs @@ -0,0 +1 @@ + diff --git a/crates/anyrender_skia/src/lib.rs b/crates/anyrender_skia/src/lib.rs new file mode 100644 index 0000000..a2d7ab9 --- /dev/null +++ b/crates/anyrender_skia/src/lib.rs @@ -0,0 +1,14 @@ +mod image_renderer; +mod scene; +mod window_renderer; + +// Backends +#[cfg(target_os = "macos")] +mod metal; +#[cfg(not(target_os = "macos"))] +mod opengl; +#[cfg(feature = "vulkan")] +mod vulkan; + +pub use scene::SkiaScenePainter; +pub use window_renderer::SkiaWindowRenderer; diff --git a/crates/anyrender_skia/src/metal.rs b/crates/anyrender_skia/src/metal.rs new file mode 100644 index 0000000..438ddd1 --- /dev/null +++ b/crates/anyrender_skia/src/metal.rs @@ -0,0 +1,125 @@ +use std::sync::Arc; + +use objc2::{rc::Retained, runtime::ProtocolObject}; +use objc2_app_kit::NSView; +use objc2_core_foundation::CGSize; +use objc2_metal::{MTLCommandBuffer, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice}; +use objc2_quartz_core::{CAMetalDrawable, CAMetalLayer}; +use skia_safe::{ + ColorType, Surface, + gpu::{self, DirectContext, SurfaceOrigin, backend_render_targets, mtl}, + scalar, +}; + +use crate::window_renderer::SkiaBackend; + +pub struct MetalBackend { + pub metal_layer: Retained, + pub command_queue: Retained>, + pub skia: DirectContext, + prepared_drawable: Option>>, +} + +impl MetalBackend { + pub fn new(window: Arc, width: u32, height: u32) -> Self { + let device = MTLCreateSystemDefaultDevice().expect("no device found"); + + let metal_layer = { + let layer = CAMetalLayer::new(); + layer.setDevice(Some(&device)); + layer.setPixelFormat(objc2_metal::MTLPixelFormat::BGRA8Unorm); + layer.setPresentsWithTransaction(false); + // Disabling this option allows Skia's Blend Mode to work. + // More about: https://developer.apple.com/documentation/quartzcore/cametallayer/1478168-framebufferonly + layer.setFramebufferOnly(false); + + let view_ptr = match window.window_handle().unwrap().as_raw() { + raw_window_handle::RawWindowHandle::AppKit(appkit) => { + appkit.ns_view.as_ptr() as *mut NSView + } + _ => panic!("Wrong window handle type"), + }; + let view = unsafe { view_ptr.as_ref().unwrap() }; + + view.setWantsLayer(true); + view.setLayer(Some(&layer.clone().into_super())); + layer.setDrawableSize(CGSize::new(width as f64, height as f64)); + layer + }; + + let command_queue = device + .newCommandQueue() + .expect("unable to get command queue"); + + let backend = unsafe { + mtl::BackendContext::new( + Retained::as_ptr(&device) as mtl::Handle, + Retained::as_ptr(&command_queue) as mtl::Handle, + ) + }; + + let skia_context = gpu::direct_contexts::make_metal(&backend, None).unwrap(); + + Self { + metal_layer, + command_queue, + skia: skia_context, + prepared_drawable: None, + } + } +} + +impl SkiaBackend for MetalBackend { + fn set_size(&mut self, width: u32, height: u32) { + self.metal_layer + .setDrawableSize(CGSize::new(width as f64, height as f64)); + } + + fn prepare(&mut self) -> Option { + let drawable = self.metal_layer.nextDrawable()?; + + let (drawable_width, drawable_height) = { + let size = self.metal_layer.drawableSize(); + (size.width as scalar, size.height as scalar) + }; + + let surface = { + let texture_info = unsafe { + mtl::TextureInfo::new(Retained::as_ptr(&drawable.texture()) as mtl::Handle) + }; + + let backend_render_target = backend_render_targets::make_mtl( + (drawable_width as i32, drawable_height as i32), + &texture_info, + ); + + gpu::surfaces::wrap_backend_render_target( + &mut self.skia, + &backend_render_target, + SurfaceOrigin::TopLeft, + ColorType::BGRA8888, + None, + None, + ) + .unwrap() + }; + + self.prepared_drawable = Some((&drawable).into()); + + Some(surface) + } + + fn flush(&mut self, surface: Surface) { + self.skia.flush_and_submit(); + drop(surface); + let command_buffer = self + .command_queue + .commandBuffer() + .expect("unsable to get command buffer"); + + // TODO: save drawable + let drawable = self.prepared_drawable.take().unwrap(); + command_buffer.presentDrawable(&drawable); + command_buffer.commit(); + } +} diff --git a/crates/anyrender_skia/src/opengl.rs b/crates/anyrender_skia/src/opengl.rs new file mode 100644 index 0000000..3846c60 --- /dev/null +++ b/crates/anyrender_skia/src/opengl.rs @@ -0,0 +1,195 @@ +use std::{ffi::CString, num::NonZeroU32, sync::Arc}; + +use glutin::display::DisplayApiPreference; +use glutin::{ + config::{ConfigTemplateBuilder, GetGlConfig, GlConfig}, + context::{ContextAttributesBuilder, PossiblyCurrentContext}, + display::{Display, GetGlDisplay}, + prelude::{GlDisplay, NotCurrentGlContext, PossiblyCurrentGlContext}, + surface::{GlSurface, SurfaceAttributesBuilder, WindowSurface}, +}; +use skia_safe::{ + Surface, + gpu::{ + DirectContext, direct_contexts, + gl::{FramebufferInfo, Interface}, + }, +}; + +use crate::window_renderer::SkiaBackend; + +pub(crate) struct OpenGLBackend { + surface: Option, + gr_context: DirectContext, + gl_surface: glutin::surface::Surface, + gl_context: PossiblyCurrentContext, + fb_info: FramebufferInfo, +} + +impl OpenGLBackend { + pub(crate) fn new( + window: Arc, + width: u32, + height: u32, + ) -> OpenGLBackend { + let raw_display_handle = window.display_handle().unwrap().as_raw(); + let raw_window_handle = window.window_handle().unwrap().as_raw(); + + let gl_display = unsafe { + Display::new( + raw_display_handle, + #[cfg(target_os = "macos")] + DisplayApiPreference::Cgl, + #[cfg(target_os = "windows")] + DisplayApiPreference::Wgl(Some(raw_window_handle.clone())), + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + DisplayApiPreference::Egl, + ) + .unwrap() + }; + + let gl_config_template = ConfigTemplateBuilder::new().with_transparency(true).build(); + let gl_config = unsafe { + gl_display + .find_configs(gl_config_template) + .unwrap() + .reduce(|accum, config| { + let transparency_check = config.supports_transparency().unwrap_or(false) + & !accum.supports_transparency().unwrap_or(false); + + if transparency_check || config.num_samples() < accum.num_samples() { + config + } else { + accum + } + }) + .unwrap() + }; + + let gl_context_attrs = ContextAttributesBuilder::new().build(Some(raw_window_handle)); + let gl_surface_attrs = SurfaceAttributesBuilder::::new().build( + raw_window_handle, + NonZeroU32::new(width).expect("width should be a positive value"), + NonZeroU32::new(height).expect("height should be a positive value"), + ); + + let gl_not_current_context = unsafe { + gl_display + .create_context(&gl_config, &gl_context_attrs) + .unwrap() + }; + + let gl_surface = unsafe { + gl_config + .display() + .create_window_surface(&gl_config, &gl_surface_attrs) + .unwrap() + }; + + let gl_context = gl_not_current_context.make_current(&gl_surface).unwrap(); + + gl::load_with(|s| { + gl_config + .display() + .get_proc_address(CString::new(s).unwrap().as_c_str()) + }); + + let interface = Interface::new_load_with(|name| { + if name == "eglGetCurrentDisplay" { + return std::ptr::null(); + } + gl_config + .display() + .get_proc_address(CString::new(name).unwrap().as_c_str()) + }) + .unwrap(); + + let mut gr_context = direct_contexts::make_gl(interface, None).unwrap(); + + let mut fb_info = { + let mut fboid: gl::types::GLint = 0; + unsafe { + gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid); + } + + skia_safe::gpu::gl::FramebufferInfo { + fboid: fboid.try_into().unwrap(), + format: skia_safe::gpu::gl::Format::RGBA8.into(), + ..Default::default() + } + }; + + OpenGLBackend { + surface: Some(Self::create_surface( + width, + height, + &mut gr_context, + &gl_surface, + &gl_context, + &mut fb_info, + )), + gr_context, + gl_surface, + gl_context, + fb_info, + } + } + + fn create_surface( + width: u32, + height: u32, + gr_context: &mut DirectContext, + gl_surface: &glutin::surface::Surface, + gl_context: &PossiblyCurrentContext, + fb_info: &mut FramebufferInfo, + ) -> Surface { + gl_surface.resize( + gl_context, + NonZeroU32::new(width).unwrap(), + NonZeroU32::new(height).unwrap(), + ); + + let backend_render_target = skia_safe::gpu::backend_render_targets::make_gl( + (width as i32, height as i32), + gl_context.config().num_samples() as usize, + gl_context.config().stencil_size() as usize, + *fb_info, + ); + + skia_safe::gpu::surfaces::wrap_backend_render_target( + gr_context, + &backend_render_target, + skia_safe::gpu::SurfaceOrigin::BottomLeft, + skia_safe::ColorType::RGBA8888, + None, + None, + ) + .unwrap() + } +} + +impl SkiaBackend for OpenGLBackend { + fn set_size(&mut self, width: u32, height: u32) { + self.surface = Some(Self::create_surface( + width, + height, + &mut self.gr_context, + &self.gl_surface, + &self.gl_context, + &mut self.fb_info, + )); + } + + fn prepare(&mut self) -> Option { + self.gl_context.make_current(&self.gl_surface).unwrap(); + self.surface.take() + } + + fn flush(&mut self, mut surface: Surface) { + self.gr_context.flush_and_submit(); + self.gl_surface.swap_buffers(&self.gl_context).unwrap(); + surface.canvas().discard(); + + self.surface = Some(surface); + } +} diff --git a/crates/anyrender_skia/src/scene.rs b/crates/anyrender_skia/src/scene.rs new file mode 100644 index 0000000..bd43c1c --- /dev/null +++ b/crates/anyrender_skia/src/scene.rs @@ -0,0 +1,695 @@ +use std::collections::HashMap; + +use anyrender::PaintScene; +use peniko::{StyleRef, color::DynamicColor}; +use skia_safe::{ + AlphaType, BlendMode, Canvas, Color, Color4f, ColorType, Data, Font, FontArguments, + FontHinting, FontMgr, GlyphId, ImageInfo, Matrix, Paint, PaintCap, PaintJoin, PaintStyle, + Point, RRect, Rect, SamplingOptions, Shader, TileMode, Typeface, + canvas::{GlyphPositions, SaveLayerRec}, + font::Edging, + font_arguments::{VariationPosition, variation_position::Coordinate}, + gradient_shader::{Interpolation, interpolation}, + image_filters::{self, CropRect}, + shaders, +}; + +pub struct SkiaScenePainter<'a> { + pub(crate) inner: &'a Canvas, + pub(crate) font_mgr: &'a mut FontMgr, + pub(crate) typeface_cache: &'a mut HashMap<(u64, u32), Typeface>, +} + +impl PaintScene for SkiaScenePainter<'_> { + fn reset(&mut self) { + self.inner.clear(Color::WHITE); + } + + fn push_layer( + &mut self, + blend: impl Into, + alpha: f32, + transform: kurbo::Affine, + clip: &impl kurbo::Shape, + ) { + let mut paint = Paint::default(); + paint.set_alpha_f(alpha); + paint.set_anti_alias(true); + paint.set_blend_mode(peniko_blend_to_skia_blend(blend.into())); + + self.inner + .save_layer(&SaveLayerRec::default().paint(&paint)); + + self.inner + .set_matrix(&kurbo_affine_to_skia_matrix(transform).into()); + + self.inner + .clip_path(&kurbo_shape_to_skia_path(clip), None, None); + } + + fn pop_layer(&mut self) { + self.inner.restore(); + } + + fn stroke<'a>( + &mut self, + style: &kurbo::Stroke, + transform: kurbo::Affine, + brush: impl Into>, + brush_transform: Option, + shape: &impl kurbo::Shape, + ) { + self.inner.save(); + self.inner + .set_matrix(&kurbo_affine_to_skia_matrix(transform).into()); + + let mut paint = anyrender_brush_to_skia_paint(brush.into(), brush_transform); + apply_peniko_style_to_skia_paint(StyleRef::Stroke(style), &mut paint); + paint.set_anti_alias(true); + + draw_kurbo_shape_to_skia_canvas(self.inner, shape, &paint, None); + + self.inner.restore(); + } + + fn fill<'a>( + &mut self, + style: peniko::Fill, + transform: kurbo::Affine, + brush: impl Into>, + brush_transform: Option, + shape: &impl kurbo::Shape, + ) { + self.inner.save(); + self.inner + .set_matrix(&kurbo_affine_to_skia_matrix(transform).into()); + + let mut paint = anyrender_brush_to_skia_paint(brush.into(), brush_transform); + paint.set_style(PaintStyle::Fill); + paint.set_anti_alias(true); + + draw_kurbo_shape_to_skia_canvas(self.inner, shape, &paint, Some(style)); + + self.inner.restore(); + } + + fn draw_glyphs<'a, 's: 'a>( + &'s mut self, + font: &'a peniko::FontData, + font_size: f32, + hint: bool, + normalized_coords: &'a [anyrender::NormalizedCoord], + style: impl Into>, + brush: impl Into>, + brush_alpha: f32, + transform: kurbo::Affine, + glyph_transform: Option, + glyphs: impl Iterator, + ) { + self.inner.save(); + self.inner + .set_matrix(&kurbo_affine_to_skia_matrix(transform).into()); + + if let Some(affine) = glyph_transform { + self.inner.concat(&kurbo_affine_to_skia_matrix(affine)); + } + + let mut paint = anyrender_brush_to_skia_paint(brush.into(), None); + apply_peniko_style_to_skia_paint(style.into(), &mut paint); + paint.set_alpha_f(brush_alpha); + paint.set_anti_alias(true); + + let font_key = (font.data.id(), font.index); + + if !self.typeface_cache.contains_key(&font_key) { + let Some(typeface) = self + .font_mgr + .new_from_data(font.data.data(), font.index as usize) + else { + let tf = Typeface::make_deserialize(font.data.data(), None); + eprintln!( + "WARNING: failed to load font {} {} {}", + font_key.0, + font_key.1, + tf.is_some() + ); + return; + }; + self.typeface_cache.insert(font_key, typeface); + } + + let original_typeface = self.typeface_cache.get(&font_key).unwrap(); + let mut normalized_typeface: Option = None; + + fn f2dot14_to_f32(raw_value: i16) -> f32 { + let int = (raw_value >> 14) as f32; + let fract = (raw_value & !(!0 << 14)) as f32 / (1 << 14) as f32; + int + fract + } + + if !normalized_coords.is_empty() { + let axes = original_typeface + .variation_design_parameters() + .unwrap_or_default(); + if !axes.is_empty() { + let coordinates: Vec = axes + .iter() + .zip(normalized_coords.iter().map(|c| f2dot14_to_f32(*c))) + .filter(|(_, value)| *value != 0.0) + .map(|(axis, factor)| { + let value = if factor < 0.0 { + lerp_f32(axis.min, axis.def, -factor) + } else { + lerp_f32(axis.def, axis.max, factor) + }; + + Coordinate { + axis: axis.tag, + value, + } + }) + .collect(); + let variation_position = VariationPosition { + coordinates: &coordinates, + }; + + normalized_typeface = Some( + original_typeface + .clone_with_arguments( + &FontArguments::new().set_variation_design_position(variation_position), + ) + .unwrap(), + ); + } + } + + let typeface = match &normalized_typeface { + Some(it) => it, + None => original_typeface, + }; + + let mut font = Font::from_typeface(typeface, font_size); + font.set_hinting(if hint { + FontHinting::Normal + } else { + FontHinting::None + }); + font.set_edging(Edging::SubpixelAntiAlias); + + let mut draw_glyphs: Vec = vec![]; + let mut draw_positions: Vec = vec![]; + + for glyph in glyphs { + draw_glyphs.push(GlyphId::from(glyph.id as u16)); + draw_positions.push(Point::new(glyph.x, glyph.y)); + } + + self.inner.draw_glyphs_at( + &draw_glyphs[..], + GlyphPositions::Points(&draw_positions[..]), + Point::new(0.0, 0.0), + &font, + &paint, + ); + + self.inner.restore(); + } + + fn draw_box_shadow( + &mut self, + transform: kurbo::Affine, + rect: kurbo::Rect, + brush: peniko::Color, + radius: f64, + std_dev: f64, + ) { + self.inner.save(); + self.inner + .set_matrix(&kurbo_affine_to_skia_matrix(transform).into()); + + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_color4f( + Color4f::new( + brush.components[0], + brush.components[1], + brush.components[2], + brush.components[3], + ), + None, + ); + paint.set_style(PaintStyle::Fill); + + paint.set_image_filter( + image_filters::blur( + (std_dev as f32, std_dev as f32), + None, + None, + CropRect::NO_CROP_RECT, + ) + .unwrap(), + ); + + let rrect = RRect::new_nine_patch( + Rect::new( + rect.x0 as f32, + rect.y0 as f32, + rect.x1 as f32, + rect.y1 as f32, + ), + radius as f32, + radius as f32, + radius as f32, + radius as f32, + ); + + self.inner.draw_rrect(rrect, &paint); + + self.inner.restore(); + } +} + +fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { + a + (b - a) * t +} + +fn apply_peniko_style_to_skia_paint<'a>(style: peniko::StyleRef<'a>, paint: &mut Paint) { + match style { + peniko::StyleRef::Fill(_) => { + paint.set_style(PaintStyle::Fill); + } + peniko::StyleRef::Stroke(stroke) => { + paint.set_style(PaintStyle::Stroke); + paint.set_stroke(true); + paint.set_stroke_width(stroke.width as f32); + paint.set_stroke_join(match stroke.join { + kurbo::Join::Bevel => PaintJoin::Bevel, + kurbo::Join::Miter => PaintJoin::Miter, + kurbo::Join::Round => PaintJoin::Round, + }); + paint.set_stroke_cap(match stroke.start_cap { + kurbo::Cap::Butt => PaintCap::Butt, + kurbo::Cap::Square => PaintCap::Square, + kurbo::Cap::Round => PaintCap::Round, + }); + } + } +} + +fn anyrender_brush_to_skia_paint<'a>( + brush: anyrender::PaintRef<'a>, + brush_transform: Option, +) -> Paint { + match brush { + anyrender::Paint::Solid(alpha_color) => Paint::new( + Color4f::new( + alpha_color.components[0], + alpha_color.components[1], + alpha_color.components[2], + alpha_color.components[3], + ), + None, + ), + anyrender::Paint::Gradient(gradient) => { + let shader = match gradient.kind { + peniko::GradientKind::Linear(linear_gradient_position) => { + let mut colors: Vec = vec![]; + let mut positions: Vec = vec![]; + + for color_stop in gradient.stops.iter() { + colors.push(peniko_to_skia_dyn_color(color_stop.color)); + positions.push(color_stop.offset); + } + let start = skpt(linear_gradient_position.start); + let end = skpt(linear_gradient_position.end); + + let interpolation = Interpolation { + color_space: peniko_to_skia_cs_tag_to_interpol_cs( + gradient.interpolation_cs, + ), + in_premul: interpolation::InPremul::Yes, + hue_method: peniko_to_skia_hue_direction_to_hue_method( + gradient.hue_direction, + ), + }; + + Shader::linear_gradient_with_interpolation( + (start, end), + (&colors[..], None), + &positions[..], + peniko_to_skia_extend_to_tile_mode(gradient.extend), + interpolation, + &brush_transform.map(kurbo_affine_to_skia_matrix), + ) + .unwrap() + } + peniko::GradientKind::Radial(radial_gradient_position) => { + let mut colors: Vec = vec![]; + let mut positions: Vec = vec![]; + + for color_stop in gradient.stops.iter() { + colors.push(peniko_to_skia_dyn_color(color_stop.color)); + positions.push(color_stop.offset); + } + + let start_center = skpt(radial_gradient_position.start_center); + let start_radius = radial_gradient_position.start_radius; + let end_center = skpt(radial_gradient_position.end_center); + let end_radius = radial_gradient_position.end_radius; + + let interpolation = Interpolation { + color_space: peniko_to_skia_cs_tag_to_interpol_cs( + gradient.interpolation_cs, + ), + in_premul: interpolation::InPremul::Yes, + hue_method: peniko_to_skia_hue_direction_to_hue_method( + gradient.hue_direction, + ), + }; + + if start_center == end_center && start_radius == end_radius { + Shader::radial_gradient_with_interpolation( + (start_center, start_radius), + (&colors[..], None), + &positions[..], + peniko_to_skia_extend_to_tile_mode(gradient.extend), + interpolation, + &brush_transform.map(kurbo_affine_to_skia_matrix), + ) + .unwrap() + } else { + Shader::two_point_conical_gradient_with_interpolation( + (start_center, start_radius), + (end_center, end_radius), + (&colors[..], None), + &positions[..], + peniko_to_skia_extend_to_tile_mode(gradient.extend), + interpolation, + &brush_transform.map(kurbo_affine_to_skia_matrix), + ) + .unwrap() + } + } + peniko::GradientKind::Sweep(sweep_gradient_position) => { + let mut colors: Vec = vec![]; + let mut positions: Vec = vec![]; + + for color_stop in gradient.stops.iter() { + colors.push(peniko_to_skia_dyn_color(color_stop.color)); + positions.push(color_stop.offset); + } + let center = skpt(sweep_gradient_position.center); + + let interpolation = Interpolation { + color_space: peniko_to_skia_cs_tag_to_interpol_cs( + gradient.interpolation_cs, + ), + in_premul: interpolation::InPremul::Yes, + hue_method: peniko_to_skia_hue_direction_to_hue_method( + gradient.hue_direction, + ), + }; + + Shader::sweep_gradient_with_interpolation( + center, + (&colors[..], None), + &positions[..], + peniko_to_skia_extend_to_tile_mode(gradient.extend), + ( + rad_to_deg(sweep_gradient_position.start_angle), + rad_to_deg(sweep_gradient_position.end_angle), + ), + interpolation, + &brush_transform.map(kurbo_affine_to_skia_matrix), + ) + .unwrap() + } + }; + + let mut paint = Paint::default(); + paint.set_shader(Some(shader)); + paint + } + anyrender::Paint::Image(brush) => { + let src_image = brush.image; + + let image_info = ImageInfo::new( + (src_image.width as i32, src_image.height as i32), + match src_image.format { + peniko::ImageFormat::Rgba8 => ColorType::RGBA8888, + peniko::ImageFormat::Bgra8 => ColorType::BGRA8888, + _ => unreachable!(), + }, + match src_image.alpha_type { + peniko::ImageAlphaType::Alpha => AlphaType::Unpremul, + peniko::ImageAlphaType::AlphaPremultiplied => AlphaType::Premul, + }, + None, + ); + let pixels = unsafe { + Data::new_bytes(src_image.data.data()) // We have to ensure the src image data lives long enough + }; + let image = skia_safe::images::raster_from_data( + &image_info, + pixels, + image_info.min_row_bytes(), + ) + .unwrap(); + + let shader = shaders::image( + image, + ( + peniko_to_skia_extend_to_tile_mode(brush.sampler.x_extend), + peniko_to_skia_extend_to_tile_mode(brush.sampler.y_extend), + ), + &SamplingOptions::default(), + &brush_transform.map(kurbo_affine_to_skia_matrix), + ); + + let mut paint = Paint::default(); + paint.set_shader(shader); + paint + } + anyrender::Paint::Custom(_) => unreachable!(), // ToDo: figure out what to do with this + } +} + +fn rad_to_deg(rad: f32) -> f32 { + if rad == 0.0 { + return 0.0; + } + + rad * 180.0 / std::f32::consts::PI +} + +fn peniko_to_skia_extend_to_tile_mode(extend: peniko::Extend) -> TileMode { + match extend { + peniko::Extend::Pad => TileMode::Clamp, + peniko::Extend::Repeat => TileMode::Repeat, + peniko::Extend::Reflect => TileMode::Mirror, + } +} + +fn peniko_to_skia_hue_direction_to_hue_method( + direction: peniko::color::HueDirection, +) -> interpolation::HueMethod { + match direction { + peniko::color::HueDirection::Shorter => interpolation::HueMethod::Shorter, + peniko::color::HueDirection::Longer => interpolation::HueMethod::Longer, + peniko::color::HueDirection::Increasing => interpolation::HueMethod::Increasing, + peniko::color::HueDirection::Decreasing => interpolation::HueMethod::Decreasing, + _ => unreachable!(), + } +} + +fn peniko_to_skia_cs_tag_to_interpol_cs( + space: peniko::color::ColorSpaceTag, +) -> interpolation::ColorSpace { + match space { + peniko::color::ColorSpaceTag::Srgb => interpolation::ColorSpace::SRGB, + peniko::color::ColorSpaceTag::LinearSrgb => interpolation::ColorSpace::SRGBLinear, + peniko::color::ColorSpaceTag::Lab => interpolation::ColorSpace::Lab, + peniko::color::ColorSpaceTag::Lch => interpolation::ColorSpace::LCH, + peniko::color::ColorSpaceTag::Hsl => interpolation::ColorSpace::HSL, + peniko::color::ColorSpaceTag::Hwb => interpolation::ColorSpace::HWB, + peniko::color::ColorSpaceTag::Oklab => interpolation::ColorSpace::OKLab, + peniko::color::ColorSpaceTag::Oklch => interpolation::ColorSpace::OKLCH, + peniko::color::ColorSpaceTag::DisplayP3 => interpolation::ColorSpace::DisplayP3, + peniko::color::ColorSpaceTag::A98Rgb => interpolation::ColorSpace::A98RGB, + peniko::color::ColorSpaceTag::ProphotoRgb => interpolation::ColorSpace::ProphotoRGB, + peniko::color::ColorSpaceTag::Rec2020 => interpolation::ColorSpace::Rec2020, + _ => interpolation::ColorSpace::SRGB, // ToDo: overview unsupported color space tags and possibly document it, for now just fallback + } +} + +fn peniko_to_skia_dyn_color(color: DynamicColor) -> Color4f { + Color4f::new( + color.components[0], + color.components[1], + color.components[2], + color.components[3], + ) +} + +fn kurbo_affine_to_skia_matrix(affine: kurbo::Affine) -> Matrix { + let m = affine.as_coeffs(); + let scale_x = m[0] as f32; + let shear_y = m[1] as f32; + let shear_x = m[2] as f32; + let scale_y = m[3] as f32; + let translate_x = m[4] as f32; + let translate_y = m[5] as f32; + + Matrix::new_all( + scale_x, + shear_x, + translate_x, + shear_y, + scale_y, + translate_y, + 0.0, + 0.0, + 1.0, + ) +} + +#[allow(deprecated)] // We need to support it even though it's deprecated +fn peniko_blend_to_skia_blend(blend_mode: peniko::BlendMode) -> BlendMode { + if blend_mode.mix == peniko::Mix::Normal || blend_mode.mix == peniko::Mix::Clip { + match blend_mode.compose { + peniko::Compose::Clear => BlendMode::Clear, + peniko::Compose::Copy => BlendMode::Src, + peniko::Compose::Dest => BlendMode::Dst, + peniko::Compose::SrcOver => BlendMode::SrcOver, + peniko::Compose::DestOver => BlendMode::DstOver, + peniko::Compose::SrcIn => BlendMode::SrcIn, + peniko::Compose::DestIn => BlendMode::DstIn, + peniko::Compose::SrcOut => BlendMode::SrcOut, + peniko::Compose::DestOut => BlendMode::DstOut, + peniko::Compose::SrcAtop => BlendMode::SrcATop, + peniko::Compose::DestAtop => BlendMode::DstATop, + peniko::Compose::Xor => BlendMode::Xor, + peniko::Compose::Plus => BlendMode::Plus, + peniko::Compose::PlusLighter => BlendMode::Plus, + } + } else { + match blend_mode.mix { + peniko::Mix::Normal => unreachable!(), // Handled above + peniko::Mix::Multiply => BlendMode::Multiply, + peniko::Mix::Screen => BlendMode::Screen, + peniko::Mix::Overlay => BlendMode::Overlay, + peniko::Mix::Darken => BlendMode::Darken, + peniko::Mix::Lighten => BlendMode::Lighten, + peniko::Mix::ColorDodge => BlendMode::ColorDodge, + peniko::Mix::ColorBurn => BlendMode::ColorBurn, + peniko::Mix::HardLight => BlendMode::HardLight, + peniko::Mix::SoftLight => BlendMode::SoftLight, + peniko::Mix::Difference => BlendMode::Difference, + peniko::Mix::Exclusion => BlendMode::Exclusion, + peniko::Mix::Hue => BlendMode::Hue, + peniko::Mix::Saturation => BlendMode::Saturation, + peniko::Mix::Color => BlendMode::Color, + peniko::Mix::Luminosity => BlendMode::Luminosity, + peniko::Mix::Clip => unreachable!(), // Handled above + } + } +} + +fn draw_kurbo_shape_to_skia_canvas( + canvas: &skia_safe::Canvas, + shape: &impl kurbo::Shape, + paint: &skia_safe::Paint, + fill_type: Option, +) { + if let Some(rect) = shape.as_rect() { + canvas.draw_rect( + Rect::new( + rect.x0 as f32, + rect.y0 as f32, + rect.x1 as f32, + rect.y1 as f32, + ), + paint, + ); + } else if let Some(rrect) = shape.as_rounded_rect() { + let rect = Rect::new( + rrect.rect().x0 as f32, + rrect.rect().y0 as f32, + rrect.rect().x1 as f32, + rrect.rect().y1 as f32, + ); + canvas.draw_rrect( + RRect::new_nine_patch( + rect, + rrect.radii().bottom_left as f32, + rrect.radii().top_left as f32, + rrect.radii().top_right as f32, + rrect.radii().bottom_right as f32, + ), + paint, + ); + } else if let Some(line) = shape.as_line() { + canvas.draw_line( + (line.p0.x as f32, line.p0.y as f32), + (line.p1.x as f32, line.p1.y as f32), + paint, + ); + } else if let Some(circle) = shape.as_circle() { + canvas.draw_circle( + (circle.center.x as f32, circle.center.y as f32), + circle.radius as f32, + paint, + ); + } else if let Some(path_els) = shape.as_path_slice() { + let mut path = kurbo_bezpath_els_to_skia_path(path_els); + if let Some(fill_type) = fill_type { + path.set_fill_type(to_skia_fill_type(fill_type)); + } + canvas.draw_path(&path, paint); + } else { + let mut path = kurbo_shape_to_skia_path(shape); + if let Some(fill_type) = fill_type { + path.set_fill_type(to_skia_fill_type(fill_type)); + } + canvas.draw_path(&path, paint); + } +} + +fn kurbo_shape_to_skia_path(shape: &impl kurbo::Shape) -> skia_safe::Path { + let mut sk_path = skia_safe::Path::new(); + for el in shape.path_elements(0.1) { + add_kurbo_bezpath_el_to_skia_path(&el, &mut sk_path); + } + sk_path +} + +fn kurbo_bezpath_els_to_skia_path(path: &[kurbo::PathEl]) -> skia_safe::Path { + let mut sk_path = skia_safe::Path::new(); + for el in path { + add_kurbo_bezpath_el_to_skia_path(el, &mut sk_path); + } + sk_path +} + +fn add_kurbo_bezpath_el_to_skia_path(path_el: &kurbo::PathEl, skia_path: &mut skia_safe::Path) { + match path_el { + kurbo::PathEl::MoveTo(p) => _ = skia_path.move_to(skpt(*p)), + kurbo::PathEl::LineTo(p) => _ = skia_path.line_to(skpt(*p)), + kurbo::PathEl::QuadTo(p1, p2) => _ = skia_path.quad_to(skpt(*p1), skpt(*p2)), + kurbo::PathEl::CurveTo(p1, p2, p3) => { + _ = skia_path.cubic_to(skpt(*p1), skpt(*p2), skpt(*p3)) + } + kurbo::PathEl::ClosePath => _ = skia_path.close(), + }; +} + +fn to_skia_fill_type(fill: peniko::Fill) -> skia_safe::PathFillType { + match fill { + peniko::Fill::NonZero => skia_safe::PathFillType::Winding, + peniko::Fill::EvenOdd => skia_safe::PathFillType::EvenOdd, + } +} + +fn skpt(p: kurbo::Point) -> skia_safe::Point { + (p.x as f32, p.y as f32).into() +} diff --git a/crates/anyrender_skia/src/vulkan.rs b/crates/anyrender_skia/src/vulkan.rs new file mode 100644 index 0000000..621682e --- /dev/null +++ b/crates/anyrender_skia/src/vulkan.rs @@ -0,0 +1,586 @@ +use ash::{ + Device, Entry, Instance, + vk::{ + API_VERSION_1_1, AccessFlags, ApplicationInfo, CommandBuffer, CommandBufferAllocateInfo, + CommandBufferBeginInfo, CommandBufferLevel, CommandPool, CommandPoolCreateFlags, + CommandPoolCreateInfo, DependencyFlags, DeviceCreateInfo, DeviceQueueCreateInfo, Handle, + ImageAspectFlags, ImageLayout, ImageMemoryBarrier, ImageSubresourceRange, + InstanceCreateInfo, KHR_SWAPCHAIN_NAME, PhysicalDevice, PhysicalDeviceFeatures, + PipelineStageFlags, PresentInfoKHR, Queue, QueueFlags, SubmitInfo, SurfaceKHR, + SwapchainKHR, make_api_version, + }, +}; +use ash::{ + khr::surface::Instance as InstanceSurfaceFns, + vk::{ + ColorSpaceKHR, CompositeAlphaFlagsKHR, Extent2D, Format, Image, ImageUsageFlags, + PresentModeKHR, SharingMode, SwapchainCreateInfoKHR, + }, +}; +use ash::{ + khr::swapchain::Device as DeviceSwapchainFns, + vk::{Fence, FenceCreateFlags, FenceCreateInfo, Semaphore, SemaphoreCreateInfo}, +}; +use ash_window::enumerate_required_extensions; +use raw_window_handle::DisplayHandle; +use skia_safe::{ + Surface, + gpu::{ + ContextOptions, DirectContext, backend_render_targets, direct_contexts, surfaces, + vk::{Alloc, BackendContext, GetProcOf, Version}, + }, +}; +use std::{ + ffi::{CStr, CString}, + sync::Arc, + u64, +}; + +use crate::window_renderer::SkiaBackend; + +pub(crate) struct VulkanBackend { + _entry: Entry, // Dont drop until backend is dropped + instance: Instance, + surface_fns: InstanceSurfaceFns, + surface: SurfaceKHR, + physical_device: PhysicalDevice, + queue_family_index: u32, + device: Arc, + queue: Queue, + swapchain: SwapchainKHR, + swapchain_fns: DeviceSwapchainFns, + swapchain_images: Vec, + swapchain_format: Format, + swapchain_extent: Extent2D, + swapchain_image_index: u32, + gr_context: DirectContext, + image_available_semaphore: Semaphore, + render_finished_semaphore: Semaphore, + in_flight_fence: Fence, + cmd_pool: CommandPool, + cmd_buf: CommandBuffer, +} + +impl VulkanBackend { + pub(crate) fn new( + window: Arc, + width: u32, + height: u32, + ) -> VulkanBackend { + let entry = unsafe { Entry::load().unwrap() }; + + let instance = create_instance(&entry, window.display_handle().unwrap()); + let surface_fns = InstanceSurfaceFns::new(&entry, &instance); + let surface = unsafe { + ash_window::create_surface( + &entry, + &instance, + window.display_handle().unwrap().as_raw(), + window.window_handle().unwrap().as_raw(), + None, + ) + .unwrap() + }; + + let (physical_device, queue_family_index) = + pick_physical_device(&instance, &surface_fns, surface); + + let (device, queue) = create_logical_device(&instance, physical_device, queue_family_index); + let device = Arc::new(device); + + let (swapchain, swapchain_fns, swapchain_images, swapchain_format, swapchain_extent) = + create_swapchain( + &instance, + &device, + physical_device, + &surface_fns, + surface, + queue_family_index, + width, + height, + None, + ); + + let gr_context = create_gr_context( + &entry, + &instance, + physical_device, + device.clone(), + queue, + queue_family_index, + ); + + let (image_available_semaphore, render_finished_semaphore, in_flight_fence) = + create_sync_objects(&device); + + let cmd_pool = unsafe { + device + .create_command_pool( + &CommandPoolCreateInfo::default().flags( + CommandPoolCreateFlags::TRANSIENT + | CommandPoolCreateFlags::RESET_COMMAND_BUFFER, + ), + None, + ) + .unwrap() + }; + + let cmd_buf = unsafe { + device + .allocate_command_buffers( + &CommandBufferAllocateInfo::default() + .command_pool(cmd_pool) + .level(CommandBufferLevel::PRIMARY) + .command_buffer_count(1), + ) + .unwrap()[0] + }; + + Self { + _entry: entry, + instance, + surface_fns, + surface, + physical_device, + queue_family_index, + device, + queue, + swapchain, + swapchain_fns, + swapchain_images, + swapchain_format, + swapchain_extent, + swapchain_image_index: 0, + gr_context, + image_available_semaphore, + render_finished_semaphore, + in_flight_fence, + cmd_pool, + cmd_buf, + } + } +} + +impl Drop for VulkanBackend { + fn drop(&mut self) { + unsafe { + self.device.device_wait_idle().unwrap(); + + self.device + .free_command_buffers(self.cmd_pool, std::slice::from_ref(&self.cmd_buf)); + self.device.destroy_command_pool(self.cmd_pool, None); + self.device + .destroy_semaphore(self.image_available_semaphore, None); + self.device + .destroy_semaphore(self.render_finished_semaphore, None); + self.device.destroy_fence(self.in_flight_fence, None); + + self.gr_context.free_gpu_resources(); + self.gr_context.release_resources_and_abandon(); + + // This causes a segmentation fault, most likely because the swapchain can be only destroyed after the window is closed + // self.swapchain_fns.destroy_swapchain(self.swapchain, None); + // self.device.destroy_device(None); + // self.surface_fns.destroy_surface(self.surface, None); + // self.instance.destroy_instance(None); + } + } +} + +impl SkiaBackend for VulkanBackend { + fn set_size(&mut self, width: u32, height: u32) { + unsafe { + self.device.device_wait_idle().unwrap(); + + let old_swapchain = self.swapchain; + + let (swapchain, swapchain_fns, swapchain_images, swapchain_format, swapchain_extent) = + create_swapchain( + &self.instance, + &self.device, + self.physical_device, + &self.surface_fns, + self.surface, + self.queue_family_index, + width, + height, + Some(old_swapchain), + ); + self.swapchain = swapchain; + self.swapchain_fns = swapchain_fns; + self.swapchain_images = swapchain_images; + self.swapchain_format = swapchain_format; + self.swapchain_extent = swapchain_extent; + + self.swapchain_fns.destroy_swapchain(old_swapchain, None); + } + } + + fn prepare(&mut self) -> Option { + let surface = unsafe { + self.device + .wait_for_fences(&[self.in_flight_fence], true, u64::MAX) + .unwrap(); + + self.device.reset_fences(&[self.in_flight_fence]).unwrap(); + + let (image_index, _) = self + .swapchain_fns + .acquire_next_image( + self.swapchain, + u64::MAX, + self.image_available_semaphore, + Fence::null(), + ) + .unwrap(); + + self.swapchain_image_index = image_index; + + let image = self.swapchain_images[image_index as usize]; + + let alloc = Alloc::default(); + let sk_image_info = skia_safe::gpu::vk::ImageInfo::new( + image.as_raw() as _, + alloc, + skia_safe::gpu::vk::ImageTiling::OPTIMAL, + skia_safe::gpu::vk::ImageLayout::UNDEFINED, + skia_safe::gpu::vk::Format::B8G8R8A8_UNORM, + 1, + None, + None, + None, + skia_safe::gpu::vk::SharingMode::EXCLUSIVE, + ); + let render_target = backend_render_targets::make_vk( + ( + self.swapchain_extent.width as i32, + self.swapchain_extent.height as i32, + ), + &sk_image_info, + ); + + surfaces::wrap_backend_render_target( + &mut self.gr_context, + &render_target, + skia_safe::gpu::SurfaceOrigin::TopLeft, + skia_safe::ColorType::BGRA8888, + None, + None, + ) + .unwrap() + }; + + Some(surface) + } + + fn flush(&mut self, surface: Surface) { + self.gr_context.flush_and_submit(); + + let image = self.swapchain_images[self.swapchain_image_index as usize]; + + unsafe { + self.device + .begin_command_buffer(self.cmd_buf, &CommandBufferBeginInfo::default()) + .unwrap(); + + let image_barrier = ImageMemoryBarrier::default() + .src_access_mask(AccessFlags::COLOR_ATTACHMENT_WRITE) + .dst_access_mask(AccessFlags::MEMORY_READ) + .old_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .new_layout(ImageLayout::PRESENT_SRC_KHR) + .image(image) + .subresource_range(ImageSubresourceRange { + aspect_mask: ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }); + + self.device.cmd_pipeline_barrier( + self.cmd_buf, + PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + PipelineStageFlags::BOTTOM_OF_PIPE, + DependencyFlags::empty(), + &[], + &[], + &[image_barrier], + ); + + self.device.end_command_buffer(self.cmd_buf).unwrap(); + }; + + let wait_semaphores = [self.image_available_semaphore]; + let wait_stages = [PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT]; + + let signal_semaphores = [self.render_finished_semaphore]; + + let submit_infos = [SubmitInfo::default() + .wait_semaphores(&wait_semaphores) + .wait_dst_stage_mask(&wait_stages) + .command_buffers(std::slice::from_ref(&self.cmd_buf)) + .signal_semaphores(&signal_semaphores)]; + + unsafe { + self.device + .queue_submit(self.queue, &submit_infos, self.in_flight_fence) + .unwrap(); + }; + + let swapchains = [self.swapchain]; + let image_indices = [self.swapchain_image_index]; + let present_info = PresentInfoKHR::default() + .wait_semaphores(&signal_semaphores) + .swapchains(&swapchains) + .image_indices(&image_indices); + + unsafe { + self.swapchain_fns + .queue_present(self.queue, &present_info) + .unwrap(); + }; + + drop(surface); + } +} + +fn create_instance(entry: &Entry, display_handle: DisplayHandle<'_>) -> Instance { + let app_name = CString::new("AnyRender").unwrap(); + let engine_name = CString::new("No Engine").unwrap(); + let app_info = ApplicationInfo::default() + .application_name(&app_name) + .application_version(make_api_version(0, 1, 0, 0)) + .engine_name(&engine_name) + .engine_version(make_api_version(0, 1, 0, 0)) + .api_version(API_VERSION_1_1); + + let extension_names = enumerate_required_extensions(display_handle.as_raw()) + .unwrap() + .to_vec(); + + let create_info = InstanceCreateInfo::default() + .application_info(&app_info) + .enabled_extension_names(&extension_names); + + unsafe { entry.create_instance(&create_info, None).unwrap() } +} + +fn pick_physical_device( + instance: &Instance, + surface_fns: &InstanceSurfaceFns, + surface: SurfaceKHR, +) -> (PhysicalDevice, u32) { + let devices = unsafe { instance.enumerate_physical_devices().unwrap() }; + devices + .into_iter() + .find_map(|physical_device| { + let queue_family_index = unsafe { + instance + .get_physical_device_queue_family_properties(physical_device) + .iter() + .enumerate() + .find_map(|(index, props)| { + let supports_graphics = props.queue_flags.contains(QueueFlags::GRAPHICS); + let supports_surface = surface_fns + .get_physical_device_surface_support( + physical_device, + index as u32, + surface, + ) + .unwrap(); + if supports_graphics && supports_surface { + Some(index as u32) + } else { + None + } + }) + .unwrap() + }; + let extensions_supported = unsafe { + instance + .enumerate_device_extension_properties(physical_device) + .map(|exts| { + exts.iter().any(|ext| { + CStr::from_ptr(ext.extension_name.as_ptr()) == KHR_SWAPCHAIN_NAME + }) + }) + .unwrap_or(false) + }; + + if extensions_supported { + Some((physical_device, queue_family_index)) + } else { + None + } + }) + .unwrap() +} + +fn create_logical_device( + instance: &Instance, + physical_device: PhysicalDevice, + queue_family_index: u32, +) -> (Device, Queue) { + let queue_priorities = [1.0f32]; + let queue_create_info = DeviceQueueCreateInfo::default() + .queue_family_index(queue_family_index) + .queue_priorities(&queue_priorities); + + let features = PhysicalDeviceFeatures::default().sample_rate_shading(true); + + let extensions = [KHR_SWAPCHAIN_NAME.as_ptr()]; + + let create_info = DeviceCreateInfo::default() + .queue_create_infos(std::slice::from_ref(&queue_create_info)) + .enabled_extension_names(&extensions) + .enabled_features(&features); + + let device = unsafe { + instance + .create_device(physical_device, &create_info, None) + .unwrap() + }; + + let queue = unsafe { device.get_device_queue(queue_family_index, 0) }; + + (device, queue) +} + +fn create_swapchain( + instance: &Instance, + device: &Device, + physical_device: PhysicalDevice, + surface_fns: &InstanceSurfaceFns, + surface: SurfaceKHR, + queue_family_index: u32, + width: u32, + height: u32, + old_swapchain: Option, +) -> ( + SwapchainKHR, + DeviceSwapchainFns, + Vec, + Format, + Extent2D, +) { + let surface_caps = unsafe { + surface_fns + .get_physical_device_surface_capabilities(physical_device, surface) + .unwrap() + }; + + let surface_formats = unsafe { + surface_fns + .get_physical_device_surface_formats(physical_device, surface) + .unwrap() + }; + + let present_modes = unsafe { + surface_fns + .get_physical_device_surface_present_modes(physical_device, surface) + .unwrap() + }; + + let format = surface_formats + .iter() + .find(|f| { + f.format == Format::B8G8R8A8_UNORM && f.color_space == ColorSpaceKHR::SRGB_NONLINEAR + }) + .unwrap(); + + let present_mode = present_modes + .iter() + .cloned() + .find(|&m| m == PresentModeKHR::MAILBOX) + .unwrap_or(PresentModeKHR::FIFO); + + let extent = Extent2D { width, height }; + let image_count = surface_caps.min_image_count.max(2); + + let create_info = SwapchainCreateInfoKHR::default() + .surface(surface) + .min_image_count(image_count) + .image_format(format.format) + .image_color_space(format.color_space) + .image_extent(extent) + .image_array_layers(1) + .image_usage( + ImageUsageFlags::COLOR_ATTACHMENT + | ImageUsageFlags::SAMPLED + | ImageUsageFlags::TRANSFER_SRC + | ImageUsageFlags::TRANSFER_DST, + ) + .image_sharing_mode(SharingMode::EXCLUSIVE) + .queue_family_indices(std::slice::from_ref(&queue_family_index)) + .pre_transform(surface_caps.current_transform) + .composite_alpha(CompositeAlphaFlagsKHR::OPAQUE) + .present_mode(present_mode) + .clipped(true) + .old_swapchain(old_swapchain.unwrap_or(SwapchainKHR::null())); + + let swapchain_fns = DeviceSwapchainFns::new(&instance, &device); + let swapchain = unsafe { swapchain_fns.create_swapchain(&create_info, None).unwrap() }; + let images = unsafe { swapchain_fns.get_swapchain_images(swapchain).unwrap() }; + + (swapchain, swapchain_fns, images, format.format, extent) +} + +fn create_gr_context( + entry: &Entry, + instance: &Instance, + physical_device: PhysicalDevice, + device: Arc, + queue: Queue, + queue_family_index: u32, +) -> DirectContext { + let get_proc = unsafe { + |gpo: GetProcOf| { + let get_device_proc_addr = instance.fp_v1_0().get_device_proc_addr; + + match gpo { + GetProcOf::Instance(instance, name) => { + let vk_instance = ash::vk::Instance::from_raw(instance as _); + entry.get_instance_proc_addr(vk_instance, name) + } + GetProcOf::Device(device, name) => { + let vk_device = ash::vk::Device::from_raw(device as _); + get_device_proc_addr(vk_device, name) + } + } + .map(|f| f as _) + .unwrap() + } + }; + + let mut backend_context = unsafe { + BackendContext::new( + instance.handle().as_raw() as _, + physical_device.as_raw() as _, + device.handle().as_raw() as _, + (queue.as_raw() as _, queue_family_index as usize), + &get_proc, + ) + }; + backend_context.set_max_api_version(Version::new(1, 1, 0)); + + let context_options = ContextOptions::default(); + + direct_contexts::make_vulkan(&backend_context, &context_options).unwrap() +} + +fn create_sync_objects(device: &Device) -> (Semaphore, Semaphore, Fence) { + let semaphore_info = SemaphoreCreateInfo::default(); + let fence_info = FenceCreateInfo::default().flags(FenceCreateFlags::SIGNALED); + + let image_available_semaphore = + unsafe { device.create_semaphore(&semaphore_info, None).unwrap() }; + let render_finished_semaphore = + unsafe { device.create_semaphore(&semaphore_info, None).unwrap() }; + let in_flight_fence = unsafe { device.create_fence(&fence_info, None).unwrap() }; + + ( + image_available_semaphore, + render_finished_semaphore, + in_flight_fence, + ) +} diff --git a/crates/anyrender_skia/src/window_renderer.rs b/crates/anyrender_skia/src/window_renderer.rs new file mode 100644 index 0000000..e54a213 --- /dev/null +++ b/crates/anyrender_skia/src/window_renderer.rs @@ -0,0 +1,108 @@ +use std::{collections::HashMap, sync::Arc}; + +use anyrender::WindowRenderer; +use debug_timer::debug_timer; +use skia_safe::{Color, FontMgr, Surface, Typeface}; + +use crate::scene::SkiaScenePainter; + +pub(crate) trait SkiaBackend { + fn set_size(&mut self, width: u32, height: u32); + + fn prepare(&mut self) -> Option; + + fn flush(&mut self, surface: Surface); +} + +enum RenderState { + Active(ActiveRenderState), + Suspended, +} + +struct ActiveRenderState { + backend: Box, + font_mgr: FontMgr, + typeface_cache: HashMap<(u64, u32), Typeface>, +} + +pub struct SkiaWindowRenderer { + render_state: RenderState, +} + +impl Default for SkiaWindowRenderer { + fn default() -> Self { + Self::new() + } +} + +impl SkiaWindowRenderer { + pub fn new() -> Self { + Self { + render_state: RenderState::Suspended, + } + } +} + +impl SkiaWindowRenderer {} + +impl WindowRenderer for SkiaWindowRenderer { + type ScenePainter<'a> + = SkiaScenePainter<'a> + where + Self: 'a; + + fn resume(&mut self, window: Arc, width: u32, height: u32) { + #[cfg(target_os = "macos")] + let backend = crate::metal::MetalBackend::new(window, width, height); + #[cfg(not(target_os = "macos"))] + let backend = crate::opengl::OpenGLBackend::new(window, width, height); + + self.render_state = RenderState::Active(ActiveRenderState { + backend: Box::new(backend), + font_mgr: FontMgr::new(), + typeface_cache: HashMap::new(), + }) + } + + fn suspend(&mut self) { + self.render_state = RenderState::Suspended; + } + + fn is_active(&self) -> bool { + matches!(self.render_state, RenderState::Active(..)) + } + + fn set_size(&mut self, width: u32, height: u32) { + if let RenderState::Active(state) = &mut self.render_state { + state.backend.set_size(width, height); + } + } + + fn render)>(&mut self, draw_fn: F) { + let RenderState::Active(state) = &mut self.render_state else { + return; + }; + + debug_timer!(timer, feature = "log_frame_times"); + + let mut surface = match state.backend.prepare() { + Some(it) => it, + None => return, + }; + + surface.canvas().restore_to_count(1); + surface.canvas().clear(Color::WHITE); + + draw_fn(&mut SkiaScenePainter { + inner: surface.canvas(), + font_mgr: &mut state.font_mgr, + typeface_cache: &mut state.typeface_cache, + }); + timer.record_time("cmd"); + + state.backend.flush(surface); + timer.record_time("render"); + + timer.print_times("Frame time: "); + } +} diff --git a/examples/winit/Cargo.toml b/examples/winit/Cargo.toml index ab881c5..0aad5ad 100644 --- a/examples/winit/Cargo.toml +++ b/examples/winit/Cargo.toml @@ -12,6 +12,7 @@ peniko = { workspace = true } wgpu = { workspace = true } anyrender = { workspace = true } anyrender_vello = { workspace = true } +anyrender_skia = { workspace = true } anyrender_vello_hybrid = { workspace = true } anyrender_vello_cpu = { workspace = true, features = [ "pixels_window_renderer", diff --git a/examples/winit/src/main.rs b/examples/winit/src/main.rs index e3a603c..b651707 100644 --- a/examples/winit/src/main.rs +++ b/examples/winit/src/main.rs @@ -1,4 +1,5 @@ use anyrender::{NullWindowRenderer, PaintScene, WindowRenderer}; +use anyrender_skia::SkiaWindowRenderer; use anyrender_vello::VelloWindowRenderer; use anyrender_vello_cpu::{PixelsWindowRenderer, SoftbufferWindowRenderer, VelloCpuImageRenderer}; use anyrender_vello_hybrid::VelloHybridWindowRenderer; @@ -22,8 +23,9 @@ struct App { type VelloCpuSBWindowRenderer = SoftbufferWindowRenderer; type VelloCpuWindowRenderer = PixelsWindowRenderer; +type InitialBackend = SkiaWindowRenderer; // type InitialBackend = VelloWindowRenderer; -type InitialBackend = VelloHybridWindowRenderer; +// type InitialBackend = VelloHybridWindowRenderer; // type InitialBackend = VelloCpuWindowRenderer; // type InitialBackend = VelloCpuSBWindowRenderer; // type InitialBackend = NullWindowRenderer; @@ -33,6 +35,7 @@ enum Renderer { Hybrid(Box), Cpu(Box), CpuSoftbuffer(Box), + Skia(Box), Null(NullWindowRenderer), } impl From for Renderer { @@ -55,6 +58,11 @@ impl From for Renderer { Self::CpuSoftbuffer(Box::new(renderer)) } } +impl From for Renderer { + fn from(renderer: SkiaWindowRenderer) -> Self { + Self::Skia(Box::new(renderer)) + } +} impl From for Renderer { fn from(renderer: NullWindowRenderer) -> Self { Self::Null(renderer) @@ -69,6 +77,7 @@ impl Renderer { Renderer::Cpu(r) => r.is_active(), Renderer::CpuSoftbuffer(r) => r.is_active(), Renderer::Null(r) => r.is_active(), + Renderer::Skia(r) => r.is_active(), } } @@ -79,6 +88,7 @@ impl Renderer { Renderer::Cpu(r) => r.set_size(w, h), Renderer::CpuSoftbuffer(r) => r.set_size(w, h), Renderer::Null(r) => r.set_size(w, h), + Renderer::Skia(r) => r.set_size(w, h), } } } @@ -193,6 +203,9 @@ impl ApplicationHandler for App { self.request_redraw(); } WindowEvent::RedrawRequested => match renderer { + Renderer::Skia(r) => { + r.render(|p| App::draw_scene(p, Color::from_rgb8(128, 128, 128))) + } Renderer::Gpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(255, 0, 0))), Renderer::Hybrid(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))), Renderer::Cpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 255, 0))), @@ -217,6 +230,9 @@ impl ApplicationHandler for App { self.set_backend(VelloWindowRenderer::new(), event_loop); } Renderer::Gpu(_) => { + self.set_backend(SkiaWindowRenderer::new(), event_loop); + } + Renderer::Skia(_) => { self.set_backend(NullWindowRenderer::new(), event_loop); } Renderer::Null(_) => {