├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── hello_skulpin_sdl2.rs ├── hello_skulpin_winit.rs ├── hello_skulpin_winit_app.rs ├── interactive_sdl2.rs ├── interactive_winit_app.rs └── physics.rs ├── fonts ├── feather.ttf ├── fontawesome-470.ttf ├── materialdesignicons-webfont.ttf └── mplus-1p-regular.ttf ├── rustfmt.toml ├── screenshot.png ├── skulpin-app-winit ├── Cargo.toml └── src │ ├── app.rs │ ├── app_control.rs │ ├── input_state.rs │ ├── lib.rs │ ├── time_state.rs │ └── util.rs ├── skulpin-renderer ├── Cargo.toml ├── shaders │ ├── compile_shaders.bat │ ├── compile_shaders.sh │ ├── glsl │ │ ├── skia.frag │ │ ├── skia.glsl │ │ └── skia.vert │ └── out │ │ ├── skia.frag.cookedshaderpackage │ │ └── skia.vert.cookedshaderpackage └── src │ ├── coordinates.rs │ ├── lib.rs │ ├── renderer.rs │ └── skia_support.rs └── src └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | toolchain: [stable, beta] 13 | os: [windows-2019, ubuntu-20.04, macos-10.15] 14 | exclude: 15 | - os: macos-10.15 16 | toolchain: beta 17 | - os: windows-2019 18 | toolchain: beta 19 | runs-on: ${{ matrix.os }} 20 | needs: clean 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: ${{ matrix.toolchain }} 27 | override: true 28 | 29 | - uses: actions/cache@v2 30 | with: 31 | path: | 32 | target 33 | key: ${{ runner.os }}-cargo-check-test-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }} 34 | 35 | # The normal build with no features (renderer only) 36 | - name: Build 37 | run: cargo check 38 | env: 39 | CARGO_INCREMENTAL: 0 40 | RUSTFLAGS: "-C debuginfo=0 -D warnings" 41 | 42 | # Verify winit 0.21 build works 43 | - name: Build winit 0.21 44 | run: cargo check --no-default-features --features=winit-21,winit-app 45 | if: ${{ runner.os == 'Linux' }} 46 | env: 47 | CARGO_INCREMENTAL: 0 48 | RUSTFLAGS: "-C debuginfo=0 -D warnings" 49 | 50 | # Verify winit 0.22 build works 51 | - name: Build winit 0.22 52 | run: cargo check --no-default-features --features=winit-22,winit-app 53 | if: ${{ runner.os == 'Linux' }} 54 | env: 55 | CARGO_INCREMENTAL: 0 56 | RUSTFLAGS: "-C debuginfo=0 -D warnings" 57 | 58 | # Verify winit 0.23 build works 59 | - name: Build winit 0.23 60 | run: cargo check --no-default-features --features=winit-23,winit-app 61 | if: ${{ runner.os == 'Linux' }} 62 | env: 63 | CARGO_INCREMENTAL: 0 64 | RUSTFLAGS: "-C debuginfo=0 -D warnings" 65 | 66 | # Run tests (within winit 0.24) 67 | - name: Run tests 68 | run: cargo test --workspace --features=winit-24,winit-app 69 | if: ${{ runner.os == 'Linux' }} 70 | env: 71 | CARGO_INCREMENTAL: 0 72 | RUSTFLAGS: "-C debuginfo=0 -D warnings" 73 | 74 | # Run tests (within winit 0.25) 75 | - name: Run tests 76 | run: cargo test --workspace --features=winit-25,winit-app 77 | if: ${{ runner.os == 'Linux' }} 78 | env: 79 | CARGO_INCREMENTAL: 0 80 | RUSTFLAGS: "-C debuginfo=0 -D warnings" 81 | 82 | clean: 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v2 86 | 87 | - uses: actions-rs/toolchain@v1 88 | with: 89 | toolchain: stable 90 | components: rustfmt, clippy 91 | override: true 92 | 93 | - name: Check the format 94 | run: cargo fmt --all -- --check 95 | 96 | - name: Run clippy 97 | run: > 98 | cargo clippy 99 | --all-targets 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.14.1 4 | 5 | * Update to rafx 0.0.14. This fixes an compile error caused by a non-semver change upstream 6 | * Add winit-25 config flag 7 | 8 | ## 0.14.0 9 | 10 | * Reuse the same canvas every frame instead of rotating between one per swapchain 11 | * Update to rafx 0.0.13 to fix a crash when resizing the window on linux 12 | 13 | ## 0.13.1 14 | 15 | * Fix a crash that occurs when minimizing 16 | 17 | ## 0.13.0 18 | 19 | * Add vsync config option 20 | 21 | ## 0.12.0 22 | 23 | * Complete rewrite! Almost all the rendering code has been removed. `skulpin` now uses 24 | `rafx`. (The vulkan backend in `rafx` was originally from `skulpin` but is much 25 | more general and user-friendly). 26 | * The plugin system and included imgui support has been dropped. The intended way to do this 27 | now is to build your own renderer using `rafx` and use `VkSkiaContext`, `VkSkiaSurface`, and 28 | coordinate system types directly. 29 | * Feature names are now consistently kabob-case (i.e. "winit-app") 30 | 31 | ## 0.11.3 32 | 33 | * Limit open-ended version of ash to fix build issue from changes in upstream crates 34 | 35 | ## 0.11.2 36 | 37 | * Add winit-24 feature flag 38 | * Limit some open-ended versions to fix build issues from changes in upstream crates 39 | 40 | ## 0.11.1 41 | 42 | * Bump imgui due to fix build issue from changes in upstream crates 43 | 44 | ## 0.11.0 45 | 46 | * Refactor the API integrations to use ash_window, simplifying the code and removing a lot of platform-specific code 47 | from the winit integration. 48 | * The imgui plugin has been updated to use open-ended versioning. This is published as skulpin-plugin-imgui 0.6.0 but 49 | does not affect the skulpin release itself as this is a dev-dependency. 50 | * Added feature flags winit-21, winit-22, winit-23, and winit-latest for easier control of what winit version to pull 51 | in. This also allows us to have version-specific code (for example to handle upstream changes in winit API) 52 | 53 | ## 0.10.1 54 | 55 | * Limit some open-ended versions to fix build issues 56 | 57 | ## 0.10.0 58 | 59 | * Improvements to validation layer handling 60 | * Search for VK_LAYER_KHRONOS_validation but fall back to VK_LAYER_LUNARG_standard_validation if it's not present 61 | * Return a failure with a better error message if neither exist 62 | * Validation is now off by default in the examples 63 | * Rust minimum version is now 1.43.0 64 | * skia-safe minimum version is now 0.30.1 65 | * Remove incorrect gamma correction from imgui examples 66 | 67 | ## 0.9.5 68 | 69 | * Bump macos dependencies (metal/cocoa) 70 | * Update to skulpin-plugin-imgui, 0.4.0 bumps imgui support from 0.3 to 0.4 71 | * This is a dev-dependency only so should not have any downstream effects for users not explicitly using it 72 | 73 | ## 0.9.4 74 | 75 | * Fix a panic on windows that can occur when using remote desktop or unplugging monitors (Reported in #47) 76 | 77 | ## 0.9.3 78 | 79 | * This was 0.9.2 accidentally republished without intended changes 80 | 81 | ## 0.9.2 82 | 83 | * Fixed cases where the vulkan validation layer not loading when it should have, and loading when it 84 | shouldn't have. 85 | 86 | ## 0.9.1 87 | 88 | * Windows-specific fix to determine scaling factor for logical vs. physical coordinates 89 | 90 | ## 0.9.0 91 | 92 | * Use ash >=0.30 and skia_safe >=0.27. While this release likely builds with 0.26, it has a known issue that can 93 | cause a crash on MacOS (https://github.com/rust-skia/rust-skia/issues/299) 94 | 95 | ## 0.8.2 96 | 97 | * Build fixes for upstream changes: 98 | * Require ash 0.29 rather than >=0.29 99 | * Pin imgui-winit-support to 0.3.0 exactly 100 | 101 | ## 0.8.1 102 | 103 | * Allow any version of winit >= 0.21 104 | 105 | ## 0.8.0 106 | * Add support for injecting command buffers into the renderer. (See `RendererPlugin`) 107 | * Add an example of using the `RendererPlugin` interface to inject imgui 108 | * The update and draw functions on skulpin apps now take a single struct containing all parameters rather than each 109 | parameter individually. This allows adding new parameters without breaking the API, and simplifies it a bit. 110 | * Mouse wheel support 111 | * App builder now accepts a custom window title 112 | 113 | ## 0.7.0 114 | * Add an abstraction layer so that multiple windowing backends can be supported. Built-in support is provided for 115 | `sdl2` and `winit` 116 | * Refactored into multiple crates: 117 | * Existing rendering code was moved to `skulpin-renderer` 118 | * Existing `winit` support was moved to `skulpin-renderer-winit` 119 | * New `sdl2` support was implemented in `skulpin-renderer-sdl2` 120 | * Existing winit-based app layer was moved to `skulpin-app-winit` 121 | * `skulpin` crate now pulls in these crates and supports turning them on/off via feature flags 122 | 123 | ## 0.6.1 124 | * Fixed an array index out of bound issue that could occur if khr::Swapchain created more images than the specified 125 | minimum 126 | 127 | ## 0.6.0 128 | * Update to winit 0.21 129 | 130 | ## 0.5.2 131 | * Implement support for wayland and xcb 132 | 133 | ## 0.5.1 134 | * This release pins winit to exactly 0.20.0-alpha6 as winit 0.20 has breaking changes 135 | 136 | ## 0.5.0 137 | * Update to 0.20.0-alpha6 (breaking change) 138 | 139 | ## 0.4.1 140 | * Limit winit to 0.20.0-alpha4 and 0.20.0-alpha5 due to breaking changes in 0.20.0-alpha6 141 | * Allow skia_safe to be any version >= 0.21, allowing downstream users to use newer versions of skia bindings without 142 | requiring the crate to be republished 143 | 144 | ## 0.4.0 145 | * Update to skia_safe 0.21 and enable all upstream features by default 146 | 147 | ## 0.3.0 148 | * Added support for selecting from among several coordinate systems, and mechanism for turning this off. 149 | * Error handling is now done via a new callback on AppHandler `fatal_error()`. The app will no longer return, which 150 | mirrors `winit` functionality 151 | * Simplification to `TimeState` 152 | * Removed dependency on several crates (`num-traits`, `strum`) 153 | * Some internal types used in rendering code were renamed to have a Vk prefix 154 | 155 | ## 0.2.3 156 | * Allow configuring PresentMode, PhysicalDeviceType, and vulkan debug layer 157 | * Swapchain is explicitly rebuilt when the window is resized 158 | * Improve error handling (more errors are passed up the chain rather than unwrapped) 159 | 160 | ## 0.2.2 161 | * Initialize Vulkan to be the highest version that's available. This avoids triggering some validation code in Skia 162 | (#13, #14, #21) 163 | * Fixes for queue family handling (#5, #12, #14, #21) 164 | * On MacOS, switch from the deprecated `metal-rs` crate to the replacement `metal` crate. 165 | * Add support for choosing between integrated or discrete GPU (or other device types) 166 | * Add support for choosing between FIFO, MAILBOX, or other presentation modes 167 | 168 | ## 0.2.1 169 | * Minimum supported Rust version is 1.36 170 | * Fix red/blue components being reversed on Windows 171 | * Fix crash in Windows that occurs when minimizing 172 | * An image barrier in the Vulkan code had a hardcoded queue family index. This is now properly 173 | using QUEUE_FAMILY_IGNORED 174 | * When swapchain is rebuilt, the old swapchain is now passed into vkCreateSwapchainKHR 175 | * Adjusted some log levels 176 | 177 | ## 0.2.0 178 | * Changes prior to 0.2.0 were not tracked. 179 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "skulpin-renderer", 4 | "skulpin-app-winit", 5 | ] 6 | 7 | [package] 8 | name = "skulpin" 9 | version = "0.14.1" 10 | authors = ["Philip Degarmo "] 11 | edition = "2018" 12 | description = "This crate provides an easy option for drawing hardware-accelerated 2D by combining Vulkan and Skia." 13 | license = "MIT OR Apache-2.0" 14 | readme = "README.md" 15 | repository = "https://github.com/aclysma/skulpin" 16 | homepage = "https://github.com/aclysma/skulpin" 17 | keywords = ["skia", "vulkan", "ash", "2d", "graphics"] 18 | categories = ["graphics", "gui", "multimedia", "rendering", "visualization"] 19 | include = [ 20 | "**/*.rs", 21 | "Cargo.toml", 22 | "shaders/*.frag", 23 | "shaders/*.vert", 24 | "shaders/*.spv", 25 | "LICENSE-APACHE", 26 | "LICENSE-MIT", 27 | "README.md" 28 | ] 29 | 30 | # Minimum required rust version: 31 | # rust="1.40" 32 | 33 | # NOTE: See README.md for implications on how feature selection will affect build-time. As of this writing, binary 34 | # builds of skia are available for ALL of the features or NONE of the features. Only selecting some of the features will 35 | # require building skia from scratch, which will greatly increase build times 36 | [features] 37 | # svg and shaper are deprecated. svg is always available, and shaper is included with textlayout. However, leaving them 38 | # since skia_safe prints informative error messages. 39 | skia-shaper = ["skulpin-renderer/shaper"] 40 | skia-svg = ["skulpin-renderer/svg"] 41 | skia-textlayout = ["skulpin-renderer/textlayout"] 42 | skia-complete = ["skulpin-renderer/complete"] 43 | 44 | winit-app = ["skulpin-app-winit"] 45 | winit-21 = ["skulpin-app-winit/winit-21"] 46 | winit-22 = ["skulpin-app-winit/winit-22"] 47 | winit-23 = ["skulpin-app-winit/winit-23"] 48 | winit-24 = ["skulpin-app-winit/winit-24"] 49 | winit-25 = ["skulpin-app-winit/winit-25"] 50 | winit-latest = ["skulpin-app-winit/winit-latest"] 51 | 52 | default = [] 53 | 54 | [dependencies] 55 | skulpin-renderer = { version = "0.14.1", path = "skulpin-renderer" } 56 | skulpin-app-winit = { version = "0.14.1", path = "skulpin-app-winit", optional = true } 57 | 58 | log="0.4" 59 | 60 | [dev-dependencies] 61 | env_logger = "0.6" 62 | 63 | # These are used for the physics demo only 64 | rapier2d = "0.5" 65 | 66 | # The skulpin-renderer-sdl2 crate doesn't specify any features, but downstream libraries can do so by including sdl2 67 | # for themselves. Examples will use the features added here 68 | sdl2-sys = { version = ">=0.33, <=0.34.2"} 69 | sdl2 = { version = ">=0.33,<0.34.3", features = ["bundled", "static-link", "raw-window-handle"] } 70 | 71 | [[example]] 72 | name = "hello_skulpin_sdl2" 73 | required-features = [] 74 | 75 | [[example]] 76 | name = "hello_skulpin_winit" 77 | required-features = ["winit-app", "winit-25"] 78 | 79 | [[example]] 80 | name = "hello_skulpin_winit_app" 81 | required-features = ["winit-app", "winit-25"] 82 | 83 | [[example]] 84 | name = "interactive_sdl2" 85 | required-features = [] 86 | 87 | [[example]] 88 | name = "interactive_winit_app" 89 | required-features = ["winit-app", "winit-25"] 90 | 91 | [[example]] 92 | name = "physics" 93 | required-features = ["winit-app", "winit-25"] 94 | 95 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # skulpin 2 | 3 | Skia + Vulkan = Skulpin 4 | 5 | This crate provides an easy option for drawing hardware-accelerated 2D by combining vulkan and skia. 6 | 7 | Please note the [status of this crate](#status) before using. 8 | 9 | [![Build Status](https://travis-ci.org/aclysma/skulpin.svg?branch=master)](https://travis-ci.org/aclysma/skulpin) 10 | ![Crates.io](https://img.shields.io/crates/v/skulpin) 11 | 12 | ![Example Screenshot](screenshot.png "Example Screenshot") 13 | 14 | This crate mainly depends on: 15 | * [rafx](https://github.com/aclysma/rafx) - A rendering framework with easy access to the vulkan backend 16 | * [skia-safe](https://github.com/rust-skia/rust-skia) - [Skia](https://skia.org) bindings for Rust 17 | 18 | NOTE: See [skia-bindings](https://crates.io/crates/skia-bindings) for more info on how a skia binary acquired. In many 19 | cases, this crate will download a binary created by their project's CI. 20 | 21 | This crate integrates with [raw-window-handle](https://crates.io/crates/raw-window-handle), which allows it to be used 22 | with sdl2, winit, and any other windowing framework that supports raw-window-handle. 23 | 24 | ## Running the Examples 25 | 26 | First, ensure that the below requirements are met depending on OS. Afterwards, the examples can be run normally. 27 | 28 | The [interactive](examples/interactive_winit_app.rs) example is good to look at for an easy way to get keyboard/mouse input. 29 | 30 | ``` 31 | # winit 0.24 32 | cargo run --example interactive_winit_app --features winit-app,winit-25 33 | 34 | # sdl2 35 | cargo run --example interactive_sdl2 36 | ``` 37 | 38 | The [physics](examples/physics.rs) demo is fun too. 39 | 40 | ``` 41 | cargo run --example physics --features winit-app,winit-25 42 | ``` 43 | 44 | Here's a video of the physics and interactive examples. 45 | 46 | [![IMAGE ALT TEXT](http://img.youtube.com/vi/El99FgGSzfg/0.jpg)](https://www.youtube.com/watch?v=El99FgGSzfg "Video of Skulpin") 47 | 48 | ## Status 49 | 50 | I am no longer using this crate for anything, so will only maintain this crate on a very minimal basis. I will review simple pull requests and offer suggestions on opened issues. However, I may fully sunset this repo at any time, particularly if the crate stops working and requires more than trivial changes to fix. Unfortunately, I have limited time to work on open source, so I want to focus it on crates that I think are most interesting to me personally. I'm open to passing ownership of the crate to someone who has made a few PRs and shown themselves to be trustworthy, and will be glad to link to a fork if someone makes one. 51 | 52 | ## Usage 53 | 54 | Currently there are two ways to use this library with `winit`. 55 | * [app](examples/hello_skulpin_winit_app.rs) - Implement the AppHandler trait and launch the app. It's simple but not as flexible. 56 | This is currently only supported when using winit. 57 | * [renderer_only](examples/hello_skulpin_winit.rs) - You manage the window and event loop yourself. Then add the renderer to 58 | draw to it. The window should be wrapped in an implementation of `skulpin::Window`. Implementations for `sdl2` and 59 | `winit` are provided. 60 | 61 | If you prefer `sdl2` you'll need to use the renderer directly. See [sdl2 renderer only](examples/hello_skulpin_sdl2.rs) 62 | 63 | Don't forget to install the prerequisites below appropriate to your platform! (See "Requirements") 64 | 65 | ## Feature Flags 66 | 67 | ### Skia-related features: 68 | * `skia-complete` - Includes all the below skia features. ** This is on by default ** 69 | * `skia-shaper` - Enables text shaping with Harfbuzz and ICU 70 | * `skia-svg` - This feature enables the SVG rendering backend 71 | * `skia-textlayout` - Makes the Skia module skparagraph available, which contains types that are used to lay out paragraphs 72 | * More information on these flags is available in the [skia-safe readme](https://crates.io/crates/skia-safe) 73 | 74 | The `skia-bindings` prebuilt binaries are only available for certain combinations of features. As of this writing, it is 75 | available for none, each feature individually, or all features enabled. The `vulkan` feature is required and implicitly 76 | used, so enabling any features individually will substantially increase build times. It's recommended to use all 77 | features (default behavior), or disable all features. (use `default-features = false`) 78 | 79 | ### Skulpin features: 80 | * `winit-app` - Include the winit app wrapper. It's less flexbile than using the renderer directly but is easy to use. 81 | 82 | If using winit-app, you MUST specify a winit version feature flag (see below) 83 | 84 | ### Winit version feature flags: 85 | * `winit-21` 86 | * `winit-22` 87 | * `winit-23` 88 | * `winit-24` 89 | * `winit-25` 90 | * `winit-latest` 91 | 92 | (These feature names match the imgui-rs crate.) 93 | 94 | ### Examples of Feature Flag Usage 95 | 96 | ``` 97 | # Pull in all skia features and support for all backends (sdl2 and winit) 98 | skulpin = "0" 99 | 100 | # Pull in all skia features but not the winit app wrapper 101 | skulpin = { version = "0", default-features = false, features = ["skia-complete"] } 102 | 103 | # Pull in all skia features and include the winit app wrapper 104 | skulpin = { version = "0", default-features = false, features = ["skia-complete", "winit-app"] } 105 | ``` 106 | 107 | ### Upstream Versioning of skia-safe 108 | 109 | Skulpin can be built and used with many versions of skia-safe. In order to be accomodating to users of the 110 | library, the required version has been left open-ended. This allows new projects to use more recent versions of these 111 | libraries while not forcing old projects to update. 112 | 113 | You can force a particular version of skia safe by using `cargo update` 114 | 115 | ``` 116 | cargo update -p skia-safe --precise 0.32 117 | ``` 118 | 119 | ## Documentation 120 | 121 | Documentation fails to build on docs.rs because the skia_safe crate requires an internet connection to build. (It will 122 | either grab skia source code, or grab a prebuilt binary.) So the best way to view docs is to build them yourself: 123 | 124 | `cargo doc -p skulpin --open` 125 | 126 | ## Requirements 127 | 128 | Minimum required rust version: **1.43.0** 129 | 130 | ### Windows 131 | 132 | * If you're using the GNU toolchain (MSVC is the default) you might run into an issue building curl. (Curl is a 133 | dependency of skia-safe bindings, which is used to download prebuilt skia binaries.) There are some 134 | [workarounds listed here](https://github.com/alexcrichton/curl-rust/issues/239). Again, this should only affect you if 135 | you are running the non-default GNU toolchain. 136 | * If you're using SDL2, see the [requirements for the SDL2 bindings](https://github.com/Rust-SDL2/rust-sdl2). The 137 | easiest method is to use the "bundled" and "static" features. To do this, add `sdl2 = { version = ">=0.33", features = 138 | ["bundled", "static-link"] }` to you Cargo.toml. These are enabled by default for the examples. 139 | * Enabling vulkan validation requires the LunarG Validation layers and a Vulkan library that is visible in your `PATH`. 140 | An easy way to get started is to use the [LunarG Vulkan SDK](https://lunarg.com/vulkan-sdk/) 141 | 142 | ### MacOS 143 | 144 | * If you're using SDL2, see the [requirements for the SDL2 bindings](https://github.com/Rust-SDL2/rust-sdl2). The 145 | easiest method is to use the "bundled" and "static" features. To do this, add `sdl2 = { version = ">=0.33", features = 146 | ["bundled", "static-link"] }` to you Cargo.toml. These are enabled by default for the examples. 147 | * Enabling vulkan validation requires the LunarG Validation layers and a Vulkan library that is visible in your `PATH`. 148 | An easy way to get started is to use the [LunarG Vulkan SDK](https://lunarg.com/vulkan-sdk/) 149 | 150 | ### Linux 151 | 152 | * If you're using SDL2, see the [requirements for the SDL2 bindings](https://github.com/Rust-SDL2/rust-sdl2). The 153 | easiest method is to use the "bundled" and "static" features. To do this, add `sdl2 = { version = ">=0.33", features = 154 | ["bundled", "static-link"] }` to you Cargo.toml. These are enabled by default for the examples. 155 | * On linux you'll also need to link against bz2, GL, fontconfig, and freetype. 156 | * On ubuntu, you could use `libbz2-dev`, `libfreetype6-dev`, `libfontconfig1-dev`, and `libgl-dev`. (And 157 | `libvulkan-dev` to pick up the Vulkan SDK) 158 | * Enabling vulkan validation requires the LunarG Validation layers and a Vulkan library that is visible in your `PATH`. 159 | An easy way to get started is to use the [LunarG Vulkan SDK](https://lunarg.com/vulkan-sdk/) 160 | 161 | ### Other Platforms 162 | 163 | It may be possible to build this for mobile platforms, but I've not investigated this yet. 164 | 165 | ## A note on High-DPI Display Support 166 | 167 | For the common case, you can draw to the skia canvas using "logical" coordinates and not worry about dpi/scaling 168 | issues. 169 | 170 | Internally, the skia surface will match the swapchain size, but this size is not necessarily LogicalSize or 171 | PhysicalSize of the window. In order to produce consistently-sized results, the renderer will apply a scaling factor to 172 | the skia canvas before handing it off to your draw implementation. 173 | 174 | ## Important configuration choices 175 | 176 | There are a few primary choices you should consider when configuring how your app runs 177 | * Coordinate System - This library can be configured to use a few different coordinate systems. 178 | - `Logical` - Use logical coordinates, which are pixels with a factor applied to count for high resolution displays 179 | - `Physical` - Use raw pixels for coordinates 180 | - `VisibleRange` - Try to fit the given range to the window 181 | - `FixedWidth` - Use the given X extents and aspect ratio to calculate Y extents 182 | - `None` - Do not modify the canvas matrix 183 | * Presentation Mode - You'll likely either want Fifo (default) or Mailbox 184 | - `Fifo` (`VK_PRESENT_MODE_FIFO_KHR`) is the default behavior and is always present on devices that fully comply to 185 | spec. This will be VSync,shouldn't ever screen tear, and will generally run at display refresh rate. 186 | - `Mailbox` (`VK_PRESENT_MODE_MAILBOX_KHR`) will render as quickly as possible. The frames are queued and the latest 187 | complete frame will be drawn. Other frames will be dropped. This rendering method will produce the lowest latency, 188 | but is not always available, and could be an unnecessary drain on battery life for laptops and mobile devices. 189 | - See `prefer_fifo_present_mode`/`prefer_mailbox_present_mode` for a simple way to choose between the two recommended 190 | options or `present_mode_priority` for full control. 191 | - For full details see documentation for `PresentMode` and the Vulkan spec. 192 | * Device Type - The most common device types will be Dedicated or Integrated. By default, a Dedicated device is chosen 193 | when available. 194 | - `Discrete` (`VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU`) - When available, this is likely to be the device with best 195 | performance 196 | - `Integrated` (`VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU`) - This will generally be more power efficient that a 197 | Discrete GPU. 198 | - I suspect the most likely case of having both would be a laptop with a discrete GPU. I would expect that 199 | favoring the integrated GPU would be better for battery life, at the cost of some performance. However I don't have 200 | a suitable device to test this. 201 | - See `prefer_integrated_gpu`/`prefer_discrete_gpu` for a simple way to choose between the two recommended options or 202 | `physical_device_type_priority` for full control 203 | - For full details see documentation for `PhysicalDeviceType` and the Vulkan spec. 204 | * Vulkan Debug Layer - Debug logging is not enabled by default 205 | - `use_vulkan_debug_layer` turns all logging on/off 206 | - `validation_layer_debug_report_flags` allows choosing specific log levels 207 | - If the Vulkan SDK is not installed, the app will fail to start if any vulkan debugging is enabled 208 | 209 | ## License 210 | 211 | Licensed under either of 212 | 213 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 214 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 215 | 216 | at your option. 217 | 218 | The fonts directory contains several fonts under their own licenses: 219 | * [Feather](https://github.com/AT-UI/feather-font), MIT 220 | * [Material Design Icons](https://materialdesignicons.com), SIL OFL 1.1 221 | * [FontAwesome 4.7.0](https://fontawesome.com/v4.7.0/license/), available under SIL OFL 1.1 222 | * [`mplus-1p-regular.ttf`](http://mplus-fonts.osdn.jp), available under its own license. 223 | 224 | [`sdl2` uses the zlib license.](https://www.libsdl.org/license.php) 225 | 226 | ### Contribution 227 | 228 | Unless you explicitly state otherwise, any contribution intentionally 229 | submitted for inclusion in the work by you, as defined in the Apache-2.0 230 | license, shall be dual licensed as above, without any additional terms or 231 | conditions. 232 | 233 | See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT). 234 | -------------------------------------------------------------------------------- /examples/hello_skulpin_sdl2.rs: -------------------------------------------------------------------------------- 1 | // This example shows how to use the renderer with SDL2 directly. 2 | 3 | use skulpin::skia_safe; 4 | use skulpin::{CoordinateSystemHelper, RendererBuilder, LogicalSize}; 5 | use sdl2::event::Event; 6 | use sdl2::keyboard::Keycode; 7 | use skulpin::rafx::api::RafxExtents2D; 8 | 9 | fn main() { 10 | // Setup logging 11 | env_logger::Builder::from_default_env() 12 | .filter_level(log::LevelFilter::Debug) 13 | .init(); 14 | 15 | // Setup SDL 16 | let sdl_context = sdl2::init().expect("Failed to initialize sdl2"); 17 | let video_subsystem = sdl_context 18 | .video() 19 | .expect("Failed to create sdl video subsystem"); 20 | 21 | // Set up the coordinate system to be fixed at 900x600, and use this as the default window size 22 | // This means the drawing code can be written as though the window is always 900x600. The 23 | // output will be automatically scaled so that it's always visible. 24 | let logical_size = LogicalSize { 25 | width: 900, 26 | height: 600, 27 | }; 28 | let scale_to_fit = skulpin::skia_safe::matrix::ScaleToFit::Center; 29 | let visible_range = skulpin::skia_safe::Rect { 30 | left: 0.0, 31 | right: logical_size.width as f32, 32 | top: 0.0, 33 | bottom: logical_size.height as f32, 34 | }; 35 | 36 | let window = video_subsystem 37 | .window("Skulpin", logical_size.width, logical_size.height) 38 | .position_centered() 39 | .allow_highdpi() 40 | .resizable() 41 | .build() 42 | .expect("Failed to create window"); 43 | log::info!("window created"); 44 | 45 | let (window_width, window_height) = window.vulkan_drawable_size(); 46 | 47 | let extents = RafxExtents2D { 48 | width: window_width, 49 | height: window_height, 50 | }; 51 | 52 | let renderer = RendererBuilder::new() 53 | .coordinate_system(skulpin::CoordinateSystem::VisibleRange( 54 | visible_range, 55 | scale_to_fit, 56 | )) 57 | .build(&window, extents); 58 | 59 | // Check if there were error setting up vulkan 60 | if let Err(e) = renderer { 61 | println!("Error during renderer construction: {:?}", e); 62 | return; 63 | } 64 | 65 | log::info!("renderer created"); 66 | 67 | let mut renderer = renderer.unwrap(); 68 | 69 | // Increment a frame count so we can render something that moves 70 | let mut frame_count = 0; 71 | 72 | log::info!("Starting window event loop"); 73 | let mut event_pump = sdl_context 74 | .event_pump() 75 | .expect("Could not create sdl event pump"); 76 | 77 | 'running: loop { 78 | for event in event_pump.poll_iter() { 79 | log::info!("{:?}", event); 80 | match event { 81 | // 82 | // Halt if the user requests to close the window 83 | // 84 | Event::Quit { .. } => break 'running, 85 | 86 | // 87 | // Close if the escape key is hit 88 | // 89 | Event::KeyDown { 90 | keycode: Some(keycode), 91 | keymod: modifiers, 92 | .. 93 | } => { 94 | log::info!("Key Down {:?} {:?}", keycode, modifiers); 95 | if keycode == Keycode::Escape { 96 | break 'running; 97 | } 98 | } 99 | 100 | _ => {} 101 | } 102 | } 103 | 104 | // 105 | // Redraw 106 | // 107 | let (window_width, window_height) = window.vulkan_drawable_size(); 108 | let extents = RafxExtents2D { 109 | width: window_width, 110 | height: window_height, 111 | }; 112 | 113 | renderer 114 | .draw(extents, 1.0, |canvas, coordinate_system_helper| { 115 | draw(canvas, &coordinate_system_helper, frame_count); 116 | frame_count += 1; 117 | }) 118 | .unwrap(); 119 | } 120 | } 121 | 122 | /// Called when winit passes us a WindowEvent::RedrawRequested 123 | fn draw( 124 | canvas: &mut skia_safe::Canvas, 125 | _coordinate_system_helper: &CoordinateSystemHelper, 126 | frame_count: i32, 127 | ) { 128 | // Generally would want to clear data every time we draw 129 | canvas.clear(skia_safe::Color::from_argb(0, 0, 0, 255)); 130 | 131 | // Floating point value constantly moving between 0..1 to generate some movement 132 | let f = ((frame_count as f32 / 30.0).sin() + 1.0) / 2.0; 133 | 134 | // Make a color to draw with 135 | let mut paint = skia_safe::Paint::new(skia_safe::Color4f::new(1.0 - f, 0.0, f, 1.0), None); 136 | paint.set_anti_alias(true); 137 | paint.set_style(skia_safe::paint::Style::Stroke); 138 | paint.set_stroke_width(2.0); 139 | 140 | // Draw a line 141 | canvas.draw_line( 142 | skia_safe::Point::new(100.0, 500.0), 143 | skia_safe::Point::new(800.0, 500.0), 144 | &paint, 145 | ); 146 | 147 | // Draw a circle 148 | canvas.draw_circle( 149 | skia_safe::Point::new(200.0 + (f * 500.0), 420.0), 150 | 50.0, 151 | &paint, 152 | ); 153 | 154 | // Draw a rectangle 155 | canvas.draw_rect( 156 | skia_safe::Rect { 157 | left: 10.0, 158 | top: 10.0, 159 | right: 890.0, 160 | bottom: 590.0, 161 | }, 162 | &paint, 163 | ); 164 | 165 | //TODO: draw_bitmap 166 | 167 | let mut font = skia_safe::Font::default(); 168 | font.set_size(100.0); 169 | 170 | canvas.draw_str("Hello Skulpin", (65, 200), &font, &paint); 171 | canvas.draw_str("Hello Skulpin", (68, 203), &font, &paint); 172 | canvas.draw_str("Hello Skulpin", (71, 206), &font, &paint); 173 | } 174 | -------------------------------------------------------------------------------- /examples/hello_skulpin_winit.rs: -------------------------------------------------------------------------------- 1 | // This example shows how to use the renderer directly. This allows full control of winit 2 | // and the update loop 3 | 4 | use skulpin::CoordinateSystemHelper; 5 | use skulpin::winit; 6 | use skulpin::skia_safe; 7 | use skulpin::rafx::api::RafxExtents2D; 8 | 9 | fn main() { 10 | // Setup logging 11 | env_logger::Builder::from_default_env() 12 | .filter_level(log::LevelFilter::Debug) 13 | .init(); 14 | 15 | // Create the winit event loop 16 | let event_loop = winit::event_loop::EventLoop::<()>::with_user_event(); 17 | 18 | // Set up the coordinate system to be fixed at 900x600, and use this as the default window size 19 | // This means the drawing code can be written as though the window is always 900x600. The 20 | // output will be automatically scaled so that it's always visible. 21 | let logical_size = winit::dpi::LogicalSize::new(900.0, 600.0); 22 | let visible_range = skulpin::skia_safe::Rect { 23 | left: 0.0, 24 | right: logical_size.width as f32, 25 | top: 0.0, 26 | bottom: logical_size.height as f32, 27 | }; 28 | let scale_to_fit = skulpin::skia_safe::matrix::ScaleToFit::Center; 29 | 30 | // Create a single window 31 | let window = winit::window::WindowBuilder::new() 32 | .with_title("Skulpin") 33 | .with_inner_size(logical_size) 34 | .build(&event_loop) 35 | .expect("Failed to create window"); 36 | 37 | let window_size = window.inner_size(); 38 | let window_extents = RafxExtents2D { 39 | width: window_size.width, 40 | height: window_size.height, 41 | }; 42 | 43 | // Create the renderer, which will draw to the window 44 | let renderer = skulpin::RendererBuilder::new() 45 | .coordinate_system(skulpin::CoordinateSystem::VisibleRange( 46 | visible_range, 47 | scale_to_fit, 48 | )) 49 | .build(&window, window_extents); 50 | 51 | // Check if there were error setting up vulkan 52 | if let Err(e) = renderer { 53 | println!("Error during renderer construction: {:?}", e); 54 | return; 55 | } 56 | 57 | let mut renderer = renderer.unwrap(); 58 | 59 | // Increment a frame count so we can render something that moves 60 | let mut frame_count = 0; 61 | 62 | // Start the window event loop. Winit will not return once run is called. We will get notified 63 | // when important events happen. 64 | event_loop.run(move |event, _window_target, control_flow| { 65 | match event { 66 | // 67 | // Halt if the user requests to close the window 68 | // 69 | winit::event::Event::WindowEvent { 70 | event: winit::event::WindowEvent::CloseRequested, 71 | .. 72 | } => *control_flow = winit::event_loop::ControlFlow::Exit, 73 | 74 | // 75 | // Close if the escape key is hit 76 | // 77 | winit::event::Event::WindowEvent { 78 | event: 79 | winit::event::WindowEvent::KeyboardInput { 80 | input: 81 | winit::event::KeyboardInput { 82 | virtual_keycode: Some(winit::event::VirtualKeyCode::Escape), 83 | .. 84 | }, 85 | .. 86 | }, 87 | .. 88 | } => *control_flow = winit::event_loop::ControlFlow::Exit, 89 | 90 | // 91 | // Request a redraw any time we finish processing events 92 | // 93 | winit::event::Event::MainEventsCleared => { 94 | // Queue a RedrawRequested event. 95 | window.request_redraw(); 96 | } 97 | 98 | // 99 | // Redraw 100 | // 101 | winit::event::Event::RedrawRequested(_window_id) => { 102 | let window_size = window.inner_size(); 103 | let window_extents = RafxExtents2D { 104 | width: window_size.width, 105 | height: window_size.height, 106 | }; 107 | 108 | if let Err(e) = renderer.draw( 109 | window_extents, 110 | window.scale_factor(), 111 | |canvas, coordinate_system_helper| { 112 | draw(canvas, coordinate_system_helper, frame_count); 113 | frame_count += 1; 114 | }, 115 | ) { 116 | println!("Error during draw: {:?}", e); 117 | *control_flow = winit::event_loop::ControlFlow::Exit 118 | } 119 | } 120 | 121 | // 122 | // Ignore all other events 123 | // 124 | _ => {} 125 | } 126 | }); 127 | } 128 | 129 | /// Called when winit passes us a WindowEvent::RedrawRequested 130 | fn draw( 131 | canvas: &mut skia_safe::Canvas, 132 | _coordinate_system_helper: CoordinateSystemHelper, 133 | frame_count: i32, 134 | ) { 135 | // Generally would want to clear data every time we draw 136 | canvas.clear(skia_safe::Color::from_argb(0, 0, 0, 255)); 137 | 138 | // Floating point value constantly moving between 0..1 to generate some movement 139 | let f = ((frame_count as f32 / 30.0).sin() + 1.0) / 2.0; 140 | 141 | // Make a color to draw with 142 | let mut paint = skia_safe::Paint::new(skia_safe::Color4f::new(1.0 - f, 0.0, f, 1.0), None); 143 | paint.set_anti_alias(true); 144 | paint.set_style(skia_safe::paint::Style::Stroke); 145 | paint.set_stroke_width(2.0); 146 | 147 | // Draw a line 148 | canvas.draw_line( 149 | skia_safe::Point::new(100.0, 500.0), 150 | skia_safe::Point::new(800.0, 500.0), 151 | &paint, 152 | ); 153 | 154 | // Draw a circle 155 | canvas.draw_circle( 156 | skia_safe::Point::new(200.0 + (f * 500.0), 420.0), 157 | 50.0, 158 | &paint, 159 | ); 160 | 161 | // Draw a rectangle 162 | canvas.draw_rect( 163 | skia_safe::Rect { 164 | left: 10.0, 165 | top: 10.0, 166 | right: 890.0, 167 | bottom: 590.0, 168 | }, 169 | &paint, 170 | ); 171 | 172 | //TODO: draw_bitmap 173 | 174 | let mut font = skia_safe::Font::default(); 175 | font.set_size(100.0); 176 | 177 | canvas.draw_str("Hello Skulpin", (65, 200), &font, &paint); 178 | canvas.draw_str("Hello Skulpin", (68, 203), &font, &paint); 179 | canvas.draw_str("Hello Skulpin", (71, 206), &font, &paint); 180 | } 181 | -------------------------------------------------------------------------------- /examples/hello_skulpin_winit_app.rs: -------------------------------------------------------------------------------- 1 | // This example shows how to use the "app" helpers to get a window open and drawing with minimal code 2 | // It's not as flexible as working with winit directly, but it's quick and simple 3 | 4 | use skulpin::CoordinateSystem; 5 | use skulpin::LogicalSize; 6 | use skulpin::skia_safe; 7 | 8 | use skulpin::app::AppBuilder; 9 | use skulpin::app::AppUpdateArgs; 10 | use skulpin::app::AppDrawArgs; 11 | use skulpin::app::AppError; 12 | use skulpin::app::AppHandler; 13 | use skulpin::app::VirtualKeyCode; 14 | 15 | fn main() { 16 | // Setup logging 17 | env_logger::Builder::from_default_env() 18 | .filter_level(log::LevelFilter::Debug) 19 | .init(); 20 | 21 | let example_app = ExampleApp::new(); 22 | 23 | // Set up the coordinate system to be fixed at 900x600, and use this as the default window size 24 | // This means the drawing code can be written as though the window is always 900x600. The 25 | // output will be automatically scaled so that it's always visible. 26 | let logical_size = LogicalSize::new(900, 600); 27 | let visible_range = skulpin::skia_safe::Rect { 28 | left: 0.0, 29 | right: logical_size.width as f32, 30 | top: 0.0, 31 | bottom: logical_size.height as f32, 32 | }; 33 | let scale_to_fit = skulpin::skia_safe::matrix::ScaleToFit::Center; 34 | 35 | AppBuilder::new() 36 | .inner_size(logical_size) 37 | .coordinate_system(CoordinateSystem::VisibleRange(visible_range, scale_to_fit)) 38 | .run(example_app); 39 | } 40 | 41 | struct ExampleApp {} 42 | 43 | impl ExampleApp { 44 | pub fn new() -> Self { 45 | ExampleApp {} 46 | } 47 | } 48 | 49 | impl AppHandler for ExampleApp { 50 | fn update( 51 | &mut self, 52 | update_args: AppUpdateArgs, 53 | ) { 54 | let input_state = update_args.input_state; 55 | let app_control = update_args.app_control; 56 | 57 | if input_state.is_key_down(VirtualKeyCode::Escape) { 58 | app_control.enqueue_terminate_process(); 59 | } 60 | } 61 | 62 | fn draw( 63 | &mut self, 64 | draw_args: AppDrawArgs, 65 | ) { 66 | let time_state = draw_args.time_state; 67 | let canvas = draw_args.canvas; 68 | 69 | // Generally would want to clear data every time we draw 70 | canvas.clear(skia_safe::Color::from_argb(0, 0, 0, 255)); 71 | 72 | // Floating point value constantly moving between 0..1 to generate some movement 73 | let f = ((time_state.update_count() as f32 / 30.0).sin() + 1.0) / 2.0; 74 | 75 | // Make a color to draw with 76 | let mut paint = skia_safe::Paint::new(skia_safe::Color4f::new(1.0 - f, 0.0, f, 1.0), None); 77 | paint.set_anti_alias(true); 78 | paint.set_style(skia_safe::paint::Style::Stroke); 79 | paint.set_stroke_width(2.0); 80 | 81 | // Draw a line 82 | canvas.draw_line( 83 | skia_safe::Point::new(100.0, 500.0), 84 | skia_safe::Point::new(800.0, 500.0), 85 | &paint, 86 | ); 87 | 88 | // Draw a circle 89 | canvas.draw_circle( 90 | skia_safe::Point::new(200.0 + (f * 500.0), 420.0), 91 | 50.0, 92 | &paint, 93 | ); 94 | 95 | // Draw a rectangle 96 | canvas.draw_rect( 97 | skia_safe::Rect { 98 | left: 10.0, 99 | top: 10.0, 100 | right: 890.0, 101 | bottom: 590.0, 102 | }, 103 | &paint, 104 | ); 105 | 106 | //TODO: draw_bitmap 107 | 108 | let mut font = skia_safe::Font::default(); 109 | font.set_size(100.0); 110 | 111 | canvas.draw_str("Hello Skulpin", (65, 200), &font, &paint); 112 | canvas.draw_str("Hello Skulpin", (68, 203), &font, &paint); 113 | canvas.draw_str("Hello Skulpin", (71, 206), &font, &paint); 114 | } 115 | 116 | fn fatal_error( 117 | &mut self, 118 | error: &AppError, 119 | ) { 120 | println!("{}", error); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /examples/interactive_sdl2.rs: -------------------------------------------------------------------------------- 1 | // This example uses the SDL2 renderer directly in an interactive way. The `interactive` demo 2 | // that uses the app abstraction is a much cleaner/easier way to do the same thing, but uses winit 3 | // instead. 4 | 5 | use skulpin::skia_safe; 6 | use skulpin::{CoordinateSystemHelper, RendererBuilder, LogicalSize}; 7 | use sdl2::event::Event; 8 | use sdl2::keyboard::Keycode; 9 | use std::collections::VecDeque; 10 | use sdl2::mouse::{MouseState, MouseButton}; 11 | 12 | use skulpin::rafx::api::raw_window_handle::HasRawWindowHandle; 13 | use skulpin::rafx::api::RafxExtents2D; 14 | 15 | #[derive(Clone, Copy)] 16 | struct Position { 17 | x: i32, 18 | y: i32, 19 | } 20 | 21 | struct PreviousClick { 22 | position: Position, 23 | time: std::time::Instant, 24 | } 25 | 26 | impl PreviousClick { 27 | fn new( 28 | position: Position, 29 | time: std::time::Instant, 30 | ) -> Self { 31 | PreviousClick { position, time } 32 | } 33 | } 34 | 35 | struct ExampleAppState { 36 | fps_text: String, 37 | current_mouse_state: MouseState, 38 | #[allow(dead_code)] 39 | previous_mouse_state: MouseState, 40 | drag_start_position: Option, 41 | previous_clicks: VecDeque, 42 | } 43 | 44 | fn main() { 45 | // Setup logging 46 | env_logger::Builder::from_default_env() 47 | .filter_level(log::LevelFilter::Debug) 48 | .init(); 49 | 50 | // Setup SDL 51 | let sdl_context = sdl2::init().expect("Failed to initialize sdl2"); 52 | let video_subsystem = sdl_context 53 | .video() 54 | .expect("Failed to create sdl video subsystem"); 55 | 56 | // Set up the coordinate system to be fixed at 900x600, and use this as the default window size 57 | // This means the drawing code can be written as though the window is always 900x600. The 58 | // output will be automatically scaled so that it's always visible. 59 | let logical_size = LogicalSize { 60 | width: 900, 61 | height: 600, 62 | }; 63 | 64 | let window = video_subsystem 65 | .window("Skulpin", logical_size.width, logical_size.height) 66 | .position_centered() 67 | .allow_highdpi() 68 | .resizable() 69 | .build() 70 | .expect("Failed to create window"); 71 | log::info!("window created"); 72 | 73 | let (window_width, window_height) = window.vulkan_drawable_size(); 74 | let extents = RafxExtents2D { 75 | width: window_width, 76 | height: window_height, 77 | }; 78 | 79 | let renderer = RendererBuilder::new() 80 | .coordinate_system(skulpin::CoordinateSystem::Physical) 81 | .build(&window, extents); 82 | 83 | // Check if there were error setting up vulkan 84 | if let Err(e) = renderer { 85 | println!("Error during renderer construction: {:?}", e); 86 | return; 87 | } 88 | 89 | log::info!("renderer created"); 90 | 91 | let mut renderer = renderer.unwrap(); 92 | 93 | // Increment a frame count so we can render something that moves 94 | let mut frame_count = 0; 95 | 96 | log::info!("Starting window event loop"); 97 | let mut event_pump = sdl_context 98 | .event_pump() 99 | .expect("Could not create sdl event pump"); 100 | 101 | let initial_mouse_state = sdl2::mouse::MouseState::new(&event_pump); 102 | 103 | let mut app_state = ExampleAppState { 104 | fps_text: "".to_string(), 105 | previous_clicks: Default::default(), 106 | current_mouse_state: initial_mouse_state, 107 | previous_mouse_state: initial_mouse_state, 108 | drag_start_position: None, 109 | }; 110 | 111 | 'running: loop { 112 | for event in event_pump.poll_iter() { 113 | log::info!("{:?}", event); 114 | match event { 115 | // 116 | // Halt if the user requests to close the window 117 | // 118 | Event::Quit { .. } => break 'running, 119 | 120 | Event::MouseButtonDown { 121 | mouse_btn, x, y, .. 122 | } => { 123 | // 124 | // Push new clicks onto the previous_clicks list 125 | // 126 | let now = std::time::Instant::now(); 127 | if mouse_btn == MouseButton::Left { 128 | let position = Position { x, y }; 129 | let previous_click = PreviousClick::new(position, now); 130 | app_state.previous_clicks.push_back(previous_click); 131 | 132 | app_state.drag_start_position = Some(position); 133 | } 134 | } 135 | 136 | Event::MouseButtonUp { mouse_btn, .. } => { 137 | // 138 | // Clear the drag if left mouse is released 139 | // 140 | if mouse_btn == MouseButton::Left { 141 | app_state.drag_start_position = None; 142 | } 143 | } 144 | 145 | // 146 | // Close if the escape key is hit 147 | // 148 | Event::KeyDown { 149 | keycode: Some(keycode), 150 | keymod: modifiers, 151 | .. 152 | } => { 153 | // 154 | // Quit if user hits escape 155 | // 156 | log::info!("Key Down {:?} {:?}", keycode, modifiers); 157 | if keycode == Keycode::Escape { 158 | break 'running; 159 | } 160 | } 161 | 162 | _ => {} 163 | } 164 | } 165 | 166 | app_state.previous_mouse_state = app_state.current_mouse_state; 167 | app_state.current_mouse_state = MouseState::new(&event_pump); 168 | 169 | update(&mut app_state); 170 | 171 | let (window_width, window_height) = window.vulkan_drawable_size(); 172 | let extents = RafxExtents2D { 173 | width: window_width, 174 | height: window_height, 175 | }; 176 | 177 | // 178 | // Redraw 179 | // 180 | renderer 181 | .draw(extents, 1.0, |canvas, coordinate_system_helper| { 182 | draw(&app_state, canvas, &coordinate_system_helper, &window); 183 | frame_count += 1; 184 | }) 185 | .unwrap(); 186 | } 187 | } 188 | 189 | fn update(app_state: &mut ExampleAppState) { 190 | let now = std::time::Instant::now(); 191 | 192 | // 193 | // Pop old clicks from the previous_clicks list 194 | // 195 | while !app_state.previous_clicks.is_empty() 196 | && (now - app_state.previous_clicks[0].time).as_secs_f32() >= 1.0 197 | { 198 | app_state.previous_clicks.pop_front(); 199 | } 200 | } 201 | 202 | fn draw( 203 | app_state: &ExampleAppState, 204 | canvas: &mut skia_safe::Canvas, 205 | _coordinate_system_helper: &CoordinateSystemHelper, 206 | _window: &dyn HasRawWindowHandle, 207 | ) { 208 | let now = std::time::Instant::now(); 209 | 210 | // Generally would want to clear data every time we draw 211 | canvas.clear(skia_safe::Color::from_argb(0, 0, 0, 255)); 212 | 213 | // Make a color to draw with 214 | let mut paint = skia_safe::Paint::new(skia_safe::Color4f::new(0.0, 1.0, 0.0, 1.0), None); 215 | paint.set_anti_alias(true); 216 | paint.set_style(skia_safe::paint::Style::Stroke); 217 | paint.set_stroke_width(2.0); 218 | 219 | // 220 | // Draw current mouse position. 221 | // 222 | canvas.draw_circle( 223 | skia_safe::Point::new( 224 | app_state.current_mouse_state.x() as f32, 225 | app_state.current_mouse_state.y() as f32, 226 | ), 227 | 15.0, 228 | &paint, 229 | ); 230 | 231 | // 232 | // Draw previous mouse clicks 233 | // 234 | for previous_click in &app_state.previous_clicks { 235 | let age = now - previous_click.time; 236 | let age = age.as_secs_f32().min(1.0).max(0.0); 237 | 238 | // Make a color that fades out as the click is further in the past 239 | let mut paint = 240 | skia_safe::Paint::new(skia_safe::Color4f::new(0.0, 1.0 - age, 0.0, 1.0), None); 241 | paint.set_anti_alias(true); 242 | paint.set_style(skia_safe::paint::Style::Stroke); 243 | paint.set_stroke_width(3.0); 244 | 245 | let position = previous_click.position; 246 | 247 | canvas.draw_circle( 248 | skia_safe::Point::new(position.x as f32, position.y as f32), 249 | 25.0, 250 | &paint, 251 | ); 252 | } 253 | 254 | // 255 | // If mouse is being dragged, draw a line to show the drag 256 | // 257 | if let Some(drag_start_position) = app_state.drag_start_position { 258 | canvas.draw_line( 259 | skia_safe::Point::new(drag_start_position.x as f32, drag_start_position.y as f32), 260 | skia_safe::Point::new( 261 | app_state.current_mouse_state.x() as f32, 262 | app_state.current_mouse_state.y() as f32, 263 | ), 264 | &paint, 265 | ); 266 | } 267 | 268 | // 269 | // Draw FPS text 270 | // 271 | let mut text_paint = skia_safe::Paint::new(skia_safe::Color4f::new(1.0, 1.0, 0.0, 1.0), None); 272 | text_paint.set_anti_alias(true); 273 | text_paint.set_style(skia_safe::paint::Style::StrokeAndFill); 274 | text_paint.set_stroke_width(1.0); 275 | 276 | let mut font = skia_safe::Font::default(); 277 | font.set_size(20.0); 278 | canvas.draw_str(app_state.fps_text.clone(), (50, 50), &font, &text_paint); 279 | canvas.draw_str("Click and drag the mouse", (50, 80), &font, &text_paint); 280 | 281 | let scale_factor = 1.0; 282 | 283 | canvas.draw_str( 284 | format!("scale factor: {}", scale_factor), 285 | (50, 110), 286 | &font, 287 | &text_paint, 288 | ); 289 | 290 | let physical_mouse_position = ( 291 | app_state.current_mouse_state.x(), 292 | app_state.current_mouse_state.y(), 293 | ); 294 | let logical_mouse_position = ( 295 | physical_mouse_position.0 as f64 / scale_factor, 296 | physical_mouse_position.1 as f64 / scale_factor, 297 | ); 298 | canvas.draw_str( 299 | format!( 300 | "mouse L: ({:.1} {:.1}) P: ({:.1} {:.1})", 301 | logical_mouse_position.0, 302 | logical_mouse_position.1, 303 | physical_mouse_position.0, 304 | physical_mouse_position.1 305 | ), 306 | (50, 140), 307 | &font, 308 | &text_paint, 309 | ); 310 | } 311 | -------------------------------------------------------------------------------- /examples/interactive_winit_app.rs: -------------------------------------------------------------------------------- 1 | // This example shows a bit more interaction with mouse input 2 | 3 | use skulpin::skia_safe; 4 | use skulpin::app::AppHandler; 5 | use skulpin::app::AppError; 6 | use skulpin::app::AppBuilder; 7 | use skulpin::app::MouseButton; 8 | use skulpin::app::VirtualKeyCode; 9 | use skulpin::app::PhysicalPosition; 10 | use skulpin::LogicalSize; 11 | use skulpin::app::AppUpdateArgs; 12 | use skulpin::app::AppDrawArgs; 13 | 14 | use std::collections::VecDeque; 15 | 16 | use skulpin::winit; 17 | use winit::dpi::LogicalPosition; 18 | 19 | fn main() { 20 | // Setup logging 21 | env_logger::Builder::from_default_env() 22 | .filter_level(log::LevelFilter::Debug) 23 | .init(); 24 | 25 | let example_app = ExampleApp::new(); 26 | 27 | AppBuilder::new() 28 | .inner_size(LogicalSize::new(900, 600)) 29 | .run(example_app); 30 | } 31 | 32 | struct PreviousClick { 33 | position: PhysicalPosition, 34 | time: std::time::Instant, 35 | } 36 | 37 | impl PreviousClick { 38 | fn new( 39 | position: PhysicalPosition, 40 | time: std::time::Instant, 41 | ) -> Self { 42 | PreviousClick { position, time } 43 | } 44 | } 45 | 46 | struct ExampleApp { 47 | last_fps_text_change: Option, 48 | fps_text: String, 49 | previous_clicks: VecDeque, 50 | } 51 | 52 | impl ExampleApp { 53 | pub fn new() -> Self { 54 | ExampleApp { 55 | last_fps_text_change: None, 56 | fps_text: "".to_string(), 57 | previous_clicks: VecDeque::new(), 58 | } 59 | } 60 | } 61 | 62 | impl AppHandler for ExampleApp { 63 | fn update( 64 | &mut self, 65 | update_args: AppUpdateArgs, 66 | ) { 67 | let time_state = update_args.time_state; 68 | let input_state = update_args.input_state; 69 | let app_control = update_args.app_control; 70 | 71 | let now = time_state.current_instant(); 72 | 73 | // 74 | // Quit if user hits escape 75 | // 76 | if input_state.is_key_down(VirtualKeyCode::Escape) { 77 | app_control.enqueue_terminate_process(); 78 | } 79 | 80 | // 81 | // Update FPS once a second 82 | // 83 | let update_text_string = match self.last_fps_text_change { 84 | Some(last_update_instant) => (now - last_update_instant).as_secs_f32() >= 1.0, 85 | None => true, 86 | }; 87 | 88 | if update_text_string { 89 | let fps = time_state.updates_per_second(); 90 | self.fps_text = format!("Fps: {:.1}", fps); 91 | self.last_fps_text_change = Some(now); 92 | } 93 | 94 | // 95 | // Pop old clicks from the previous_clicks list 96 | // 97 | while !self.previous_clicks.is_empty() 98 | && (now - self.previous_clicks[0].time).as_secs_f32() >= 1.0 99 | { 100 | self.previous_clicks.pop_front(); 101 | } 102 | 103 | // 104 | // Push new clicks onto the previous_clicks list 105 | // 106 | if input_state.is_mouse_just_down(MouseButton::Left) { 107 | let previous_click = PreviousClick::new(input_state.mouse_position(), now); 108 | 109 | self.previous_clicks.push_back(previous_click); 110 | } 111 | } 112 | 113 | fn draw( 114 | &mut self, 115 | draw_args: AppDrawArgs, 116 | ) { 117 | let time_state = draw_args.time_state; 118 | let canvas = draw_args.canvas; 119 | let input_state = draw_args.input_state; 120 | 121 | let now = time_state.current_instant(); 122 | 123 | // Generally would want to clear data every time we draw 124 | canvas.clear(skia_safe::Color::from_argb(0, 0, 0, 255)); 125 | 126 | // Make a color to draw with 127 | let mut paint = skia_safe::Paint::new(skia_safe::Color4f::new(0.0, 1.0, 0.0, 1.0), None); 128 | paint.set_anti_alias(true); 129 | paint.set_style(skia_safe::paint::Style::Stroke); 130 | paint.set_stroke_width(2.0); 131 | 132 | // 133 | // Draw current mouse position. 134 | // 135 | let mouse_position: LogicalPosition = input_state 136 | .mouse_position() 137 | .to_logical(input_state.scale_factor()); 138 | canvas.draw_circle( 139 | skia_safe::Point::new(mouse_position.x as f32, mouse_position.y as f32), 140 | 15.0, 141 | &paint, 142 | ); 143 | 144 | // 145 | // Draw previous mouse clicks 146 | // 147 | for previous_click in &self.previous_clicks { 148 | let age = now - previous_click.time; 149 | let age = age.as_secs_f32().min(1.0).max(0.0); 150 | 151 | // Make a color that fades out as the click is further in the past 152 | let mut paint = 153 | skia_safe::Paint::new(skia_safe::Color4f::new(0.0, 1.0 - age, 0.0, 1.0), None); 154 | paint.set_anti_alias(true); 155 | paint.set_style(skia_safe::paint::Style::Stroke); 156 | paint.set_stroke_width(3.0); 157 | 158 | let position: LogicalPosition = previous_click 159 | .position 160 | .to_logical(input_state.scale_factor()); 161 | 162 | canvas.draw_circle( 163 | skia_safe::Point::new(position.x as f32, position.y as f32), 164 | 25.0, 165 | &paint, 166 | ); 167 | } 168 | 169 | // 170 | // If mouse is being dragged, draw a line to show the drag 171 | // 172 | if let Some(drag) = input_state.mouse_drag_in_progress(MouseButton::Left) { 173 | let begin_position: LogicalPosition = 174 | drag.begin_position.to_logical(input_state.scale_factor()); 175 | let end_position: LogicalPosition = 176 | drag.end_position.to_logical(input_state.scale_factor()); 177 | 178 | canvas.draw_line( 179 | skia_safe::Point::new(begin_position.x, begin_position.y), 180 | skia_safe::Point::new(end_position.x, end_position.y), 181 | &paint, 182 | ); 183 | } 184 | 185 | // 186 | // Draw FPS text 187 | // 188 | let mut text_paint = 189 | skia_safe::Paint::new(skia_safe::Color4f::new(1.0, 1.0, 0.0, 1.0), None); 190 | text_paint.set_anti_alias(true); 191 | text_paint.set_style(skia_safe::paint::Style::StrokeAndFill); 192 | text_paint.set_stroke_width(1.0); 193 | 194 | let mut font = skia_safe::Font::default(); 195 | font.set_size(20.0); 196 | canvas.draw_str(self.fps_text.clone(), (50, 50), &font, &text_paint); 197 | canvas.draw_str("Click and drag the mouse", (50, 80), &font, &text_paint); 198 | canvas.draw_str( 199 | format!("scale factor: {}", input_state.scale_factor()), 200 | (50, 110), 201 | &font, 202 | &text_paint, 203 | ); 204 | let physical_mouse_position = input_state.mouse_position(); 205 | let logical_mouse_position = 206 | physical_mouse_position.to_logical::(input_state.scale_factor()); 207 | canvas.draw_str( 208 | format!( 209 | "mouse L: ({:.1} {:.1}) P: ({:.1} {:.1})", 210 | logical_mouse_position.x, 211 | logical_mouse_position.y, 212 | physical_mouse_position.x, 213 | physical_mouse_position.y 214 | ), 215 | (50, 140), 216 | &font, 217 | &text_paint, 218 | ); 219 | } 220 | 221 | fn fatal_error( 222 | &mut self, 223 | error: &AppError, 224 | ) { 225 | println!("{}", error); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /examples/physics.rs: -------------------------------------------------------------------------------- 1 | // This example does a physics demo, because physics is fun :) 2 | 3 | use skulpin::skia_safe; 4 | 5 | use skulpin::app::AppHandler; 6 | use skulpin::app::AppUpdateArgs; 7 | use skulpin::app::AppDrawArgs; 8 | use skulpin::app::AppError; 9 | use skulpin::app::AppBuilder; 10 | use skulpin::app::VirtualKeyCode; 11 | 12 | use skulpin::LogicalSize; 13 | 14 | // Used for physics 15 | type Vector2 = rapier2d::na::Vector2; 16 | use rapier2d::dynamics::{ 17 | JointSet, RigidBodySet, IntegrationParameters, RigidBodyBuilder, RigidBodyHandle, 18 | }; 19 | use rapier2d::geometry::{BroadPhase, NarrowPhase, ColliderSet, ColliderBuilder}; 20 | use rapier2d::pipeline::PhysicsPipeline; 21 | 22 | // use ncollide2d::shape::{Cuboid, ShapeHandle, Ball}; 23 | // use nphysics2d::object::{ 24 | // ColliderDesc, RigidBodyDesc, DefaultBodySet, DefaultColliderSet, Ground, BodyPartHandle, 25 | // DefaultBodyHandle, 26 | // }; 27 | // use nphysics2d::force_generator::DefaultForceGeneratorSet; 28 | // use nphysics2d::joint::DefaultJointConstraintSet; 29 | // use nphysics2d::world::{DefaultMechanicalWorld, DefaultGeometricalWorld}; 30 | 31 | const GROUND_THICKNESS: f32 = 0.2; 32 | const GROUND_HALF_EXTENTS_WIDTH: f32 = 3.0; 33 | const BALL_RADIUS: f32 = 0.2; 34 | const GRAVITY: f32 = -9.81; 35 | const BALL_COUNT: usize = 5; 36 | 37 | // Will contain all the physics simulation state 38 | struct Physics { 39 | // geometrical_world: DefaultGeometricalWorld, 40 | // mechanical_world: DefaultMechanicalWorld, 41 | // 42 | // bodies: DefaultBodySet, 43 | // colliders: DefaultColliderSet, 44 | // joint_constraints: DefaultJointConstraintSet, 45 | // force_generators: DefaultForceGeneratorSet, 46 | // 47 | circle_body_handles: Vec, 48 | 49 | physics_pipeline: PhysicsPipeline, 50 | gravity: Vector2, 51 | integration_parameters: IntegrationParameters, 52 | broad_phase: BroadPhase, 53 | narrow_phase: NarrowPhase, 54 | rigid_body_set: RigidBodySet, 55 | collider_set: ColliderSet, 56 | joint_set: JointSet, 57 | 58 | last_update: std::time::Instant, 59 | accumulated_time: f64, 60 | } 61 | 62 | impl Physics { 63 | fn new() -> Self { 64 | // 65 | // Basic physics system setup 66 | // 67 | let physics_pipeline = PhysicsPipeline::new(); 68 | let gravity = Vector2::new(0.0, GRAVITY); 69 | let integration_parameters = IntegrationParameters::default(); 70 | let broad_phase = BroadPhase::new(); 71 | let narrow_phase = NarrowPhase::new(); 72 | let mut rigid_body_set = RigidBodySet::new(); 73 | let mut collider_set = ColliderSet::new(); 74 | let joint_set = JointSet::new(); 75 | 76 | // 77 | // Create the "ground" 78 | // 79 | let ground_body = RigidBodyBuilder::new_static() 80 | .translation(0.0, -GROUND_THICKNESS) 81 | .build(); 82 | 83 | let ground_body_handle = rigid_body_set.insert(ground_body); 84 | 85 | let ground_collider = 86 | ColliderBuilder::cuboid(GROUND_HALF_EXTENTS_WIDTH, GROUND_THICKNESS).build(); 87 | collider_set.insert(ground_collider, ground_body_handle, &mut rigid_body_set); 88 | 89 | // 90 | // Create falling objects 91 | // 92 | let shift = (BALL_RADIUS + 0.01) * 2.0; 93 | let centerx_base = shift * (BALL_COUNT as f32) / 2.0; 94 | let centery = shift / 2.0; 95 | let height = 3.0; 96 | 97 | let mut circle_body_handles = vec![]; 98 | 99 | for i in 0usize..BALL_COUNT { 100 | for j in 0usize..BALL_COUNT { 101 | // Vary the x so the balls don't stack 102 | let centerx = if j % 2 == 0 { 103 | centerx_base + 0.1 104 | } else { 105 | centerx_base - 0.1 106 | }; 107 | 108 | let x = i as f32 * shift - centerx; 109 | let y = j as f32 * shift + centery + height; 110 | 111 | let rigid_body = RigidBodyBuilder::new_dynamic().translation(x, y).build(); 112 | 113 | let rigid_body_handle = rigid_body_set.insert(rigid_body); 114 | 115 | let ball_collider = ColliderBuilder::ball(BALL_RADIUS).density(1.0).build(); 116 | 117 | // Insert the collider to the body set. 118 | collider_set.insert(ball_collider, rigid_body_handle, &mut rigid_body_set); 119 | 120 | circle_body_handles.push(rigid_body_handle); 121 | } 122 | } 123 | 124 | let last_update = std::time::Instant::now(); 125 | 126 | Physics { 127 | physics_pipeline, 128 | gravity, 129 | integration_parameters, 130 | broad_phase, 131 | narrow_phase, 132 | rigid_body_set, 133 | collider_set, 134 | joint_set, 135 | circle_body_handles, 136 | last_update, 137 | accumulated_time: 0.0, 138 | } 139 | } 140 | 141 | fn update(&mut self) { 142 | let now = std::time::Instant::now(); 143 | let time_since_last_update = (now - self.last_update).as_secs_f64(); 144 | self.accumulated_time += time_since_last_update; 145 | self.last_update = now; 146 | 147 | const STEP_TIME: f64 = 1.0 / 60.0; 148 | while self.accumulated_time > STEP_TIME { 149 | self.accumulated_time -= STEP_TIME; 150 | 151 | // Run the simulation. 152 | self.physics_pipeline.step( 153 | &self.gravity, 154 | &self.integration_parameters, 155 | &mut self.broad_phase, 156 | &mut self.narrow_phase, 157 | &mut self.rigid_body_set, 158 | &mut self.collider_set, 159 | &mut self.joint_set, 160 | None, 161 | None, 162 | &(), 163 | ); 164 | } 165 | } 166 | } 167 | 168 | fn main() { 169 | // Setup logging 170 | env_logger::Builder::from_default_env() 171 | .filter_level(log::LevelFilter::Debug) 172 | .init(); 173 | 174 | let example_app = ExampleApp::new(); 175 | 176 | AppBuilder::new() 177 | .inner_size(LogicalSize::new(900, 600)) 178 | .run(example_app); 179 | } 180 | 181 | struct ExampleApp { 182 | last_fps_text_change: Option, 183 | fps_text: String, 184 | physics: Physics, 185 | circle_colors: Vec, 186 | } 187 | 188 | impl ExampleApp { 189 | pub fn new() -> Self { 190 | fn create_circle_paint(color: skia_safe::Color4f) -> skia_safe::Paint { 191 | let mut paint = skia_safe::Paint::new(color, None); 192 | paint.set_anti_alias(true); 193 | paint.set_style(skia_safe::paint::Style::Stroke); 194 | paint.set_stroke_width(0.02); 195 | paint 196 | } 197 | 198 | let circle_colors = vec![ 199 | create_circle_paint(skia_safe::Color4f::new(0.2, 1.0, 0.2, 1.0)), 200 | create_circle_paint(skia_safe::Color4f::new(1.0, 1.0, 0.2, 1.0)), 201 | create_circle_paint(skia_safe::Color4f::new(1.0, 0.2, 0.2, 1.0)), 202 | create_circle_paint(skia_safe::Color4f::new(0.2, 0.2, 1.0, 1.0)), 203 | ]; 204 | 205 | ExampleApp { 206 | last_fps_text_change: None, 207 | fps_text: "".to_string(), 208 | physics: Physics::new(), 209 | circle_colors, 210 | } 211 | } 212 | } 213 | 214 | impl AppHandler for ExampleApp { 215 | fn update( 216 | &mut self, 217 | update_args: AppUpdateArgs, 218 | ) { 219 | let time_state = update_args.time_state; 220 | let input_state = update_args.input_state; 221 | let app_control = update_args.app_control; 222 | 223 | let now = time_state.current_instant(); 224 | 225 | // 226 | // Quit if user hits escape 227 | // 228 | if input_state.is_key_down(VirtualKeyCode::Escape) { 229 | app_control.enqueue_terminate_process(); 230 | } 231 | 232 | // 233 | // Update FPS once a second 234 | // 235 | let update_text_string = match self.last_fps_text_change { 236 | Some(last_update_instant) => (now - last_update_instant).as_secs_f32() >= 1.0, 237 | None => true, 238 | }; 239 | 240 | // Refresh FPS text 241 | if update_text_string { 242 | let fps = time_state.updates_per_second(); 243 | self.fps_text = format!("Fps: {:.1}", fps); 244 | self.last_fps_text_change = Some(now); 245 | } 246 | 247 | // Update physics 248 | self.physics.update(); 249 | } 250 | 251 | fn draw( 252 | &mut self, 253 | draw_args: AppDrawArgs, 254 | ) { 255 | let coordinate_system_helper = draw_args.coordinate_system_helper; 256 | let canvas = draw_args.canvas; 257 | 258 | let x_half_extents = GROUND_HALF_EXTENTS_WIDTH * 1.5; 259 | let y_half_extents = x_half_extents 260 | / (coordinate_system_helper.surface_extents().width as f32 261 | / coordinate_system_helper.surface_extents().height as f32); 262 | 263 | coordinate_system_helper 264 | .use_visible_range( 265 | canvas, 266 | skia_safe::Rect { 267 | left: -x_half_extents, 268 | right: x_half_extents, 269 | top: y_half_extents + 1.0, 270 | bottom: -y_half_extents + 1.0, 271 | }, 272 | skia_safe::matrix::ScaleToFit::Center, 273 | ) 274 | .unwrap(); 275 | 276 | // Generally would want to clear data every time we draw 277 | canvas.clear(skia_safe::Color::from_argb(255, 0, 0, 0)); 278 | 279 | // Make a color to draw with 280 | let mut paint = skia_safe::Paint::new(skia_safe::Color4f::new(0.0, 1.0, 0.0, 1.0), None); 281 | paint.set_anti_alias(true); 282 | paint.set_style(skia_safe::paint::Style::Stroke); 283 | paint.set_stroke_width(0.02); 284 | 285 | canvas.draw_rect( 286 | skia_safe::Rect { 287 | left: -GROUND_HALF_EXTENTS_WIDTH, 288 | top: 0.0, 289 | right: GROUND_HALF_EXTENTS_WIDTH, 290 | bottom: -GROUND_THICKNESS, 291 | }, 292 | &paint, 293 | ); 294 | 295 | for (i, circle_body) in self.physics.circle_body_handles.iter().enumerate() { 296 | let position = self 297 | .physics 298 | .rigid_body_set 299 | .get(*circle_body) 300 | .unwrap() 301 | .position() 302 | .translation; 303 | 304 | let paint = &self.circle_colors[i % self.circle_colors.len()]; 305 | 306 | canvas.draw_circle( 307 | skia_safe::Point::new(position.x, position.y), 308 | BALL_RADIUS, 309 | paint, 310 | ); 311 | } 312 | 313 | coordinate_system_helper.use_logical_coordinates(canvas); 314 | 315 | // 316 | // Draw FPS text 317 | // 318 | let mut text_paint = 319 | skia_safe::Paint::new(skia_safe::Color4f::new(1.0, 1.0, 0.0, 1.0), None); 320 | text_paint.set_anti_alias(true); 321 | text_paint.set_style(skia_safe::paint::Style::StrokeAndFill); 322 | text_paint.set_stroke_width(1.0); 323 | 324 | let mut font = skia_safe::Font::default(); 325 | font.set_size(20.0); 326 | canvas.draw_str(self.fps_text.clone(), (50, 50), &font, &text_paint); 327 | } 328 | 329 | fn fatal_error( 330 | &mut self, 331 | error: &AppError, 332 | ) { 333 | println!("{}", error); 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /fonts/feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aclysma/skulpin/6a7fa663ead00e875e3e290be5a67b2c74e10a31/fonts/feather.ttf -------------------------------------------------------------------------------- /fonts/fontawesome-470.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aclysma/skulpin/6a7fa663ead00e875e3e290be5a67b2c74e10a31/fonts/fontawesome-470.ttf -------------------------------------------------------------------------------- /fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aclysma/skulpin/6a7fa663ead00e875e3e290be5a67b2c74e10a31/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /fonts/mplus-1p-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aclysma/skulpin/6a7fa663ead00e875e3e290be5a67b2c74e10a31/fonts/mplus-1p-regular.ttf -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | 2 | # Many of the imports and modules are in "conceptual" order, for example 3 | # similar imports/modules are next to each other in order. 4 | reorder_imports = false 5 | reorder_modules = false 6 | 7 | # This makes merge conflicts less likely to occur, and makes it easier to see 8 | # symmetry between parameters. 9 | fn_args_layout = "Vertical" 10 | 11 | # Once TODOs are cleared we can turn this on 12 | # report_todo = "Unnumbered" 13 | # report_fixme = "Unnumbered" 14 | 15 | # Things I'd like to turn on if it's ever supported in cargo fmt 16 | # 17 | # ----- Force unsafe blocks to be multi-line ----- 18 | # 19 | # I'd like for unsafe blocks to be forced onto new lines, both before 20 | # the unsafe and after the "unsafe {". New line before the unsafe 21 | # could be omitted in simple let statements 22 | # 23 | # CURRENT BEHAVIOR 24 | # 25 | # semaphors.push(unsafe { vkCreateSemaphore() }); 26 | # 27 | # let x = unsafe { get_x() }; 28 | # 29 | # DESIRED BEHAVIOR 30 | # 31 | # semaphors.push( 32 | # unsafe { 33 | # vkCreateSemaphore(); 34 | # } 35 | # ) 36 | # 37 | # let x = unsafe { 38 | # get_x(); 39 | # } 40 | # 41 | # RATIONALE 42 | # 43 | # This would make unsafe code more conspicuous. One workaround for 44 | # particularly bad cases like the first example is to break it to 45 | # multiple lines. 46 | # 47 | # ----- fn_args_layout for callsites ----- 48 | # 49 | # I'd like the "vetical" behavior for fn_args_layout applied at callsites 50 | # as well 51 | # 52 | # CURRENT BEHAVIOR 53 | # 54 | # draw_circle(radius, position, color); 55 | # 56 | # LogicalPosition::new(p0.x - p1.x, p0.y - p1.y) 57 | # 58 | # DESIRED BEHAVIOR 59 | # 60 | # draw_circle 61 | # radius, 62 | # position, 63 | # color 64 | # ); 65 | # 66 | # LogicalPosition::new( 67 | # p0.x - p1.x, 68 | # p0.y - p1.y 69 | # ); 70 | # 71 | # RATIONALE 72 | # 73 | # It's not uncommon to insert new parameters, or change parameters, 74 | # being passed into a function. Merge conflicts are less likely to 75 | # happen if each argument gets its own line. 76 | # 77 | # In some cases there is symmetry that is helpful to see. When the 78 | # the symmetry is broken, it's more conspicuous. This can make bugs 79 | # easier to spot, or if it's not a bug, help the reader to see this. 80 | # 81 | # As a bonus it's also more consistent with fn_args_layout 82 | # -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aclysma/skulpin/6a7fa663ead00e875e3e290be5a67b2c74e10a31/screenshot.png -------------------------------------------------------------------------------- /skulpin-app-winit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "skulpin-app-winit" 3 | version = "0.14.1" 4 | authors = ["Philip Degarmo "] 5 | edition = "2018" 6 | description = "A winit-based application layer for skulpin" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/aclysma/skulpin" 9 | homepage = "https://github.com/aclysma/skulpin" 10 | keywords = ["skia", "vulkan", "ash", "2d", "graphics"] 11 | categories = ["graphics", "gui", "multimedia", "rendering", "visualization"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | skulpin-renderer = { version = "0.14.1", path = "../skulpin-renderer" } 17 | 18 | log="0.4" 19 | 20 | winit-21 = { package = "winit", version = "0.21", optional = true } 21 | winit-22 = { package = "winit", version = "0.22", optional = true } 22 | winit-23 = { package = "winit", version = "0.23", optional = true } 23 | winit-24 = { package = "winit", version = "0.24", optional = true } 24 | winit-25 = { package = "winit", version = "0.25", optional = true } 25 | winit-latest = { package = "winit", version = ">=0.23", optional = true } 26 | raw-window-handle = "0.3" 27 | 28 | [features] 29 | -------------------------------------------------------------------------------- /skulpin-app-winit/src/app.rs: -------------------------------------------------------------------------------- 1 | //! Contains the main types a user needs to interact with to configure and run a skulpin app 2 | 3 | use crate::skia_safe; 4 | use crate::winit; 5 | 6 | use super::app_control::AppControl; 7 | use super::input_state::InputState; 8 | use super::time_state::TimeState; 9 | use super::util::PeriodicEvent; 10 | 11 | use skulpin_renderer::LogicalSize; 12 | use skulpin_renderer::Size; 13 | use skulpin_renderer::RendererBuilder; 14 | use skulpin_renderer::CoordinateSystem; 15 | use skulpin_renderer::CoordinateSystemHelper; 16 | use skulpin_renderer::ValidationMode; 17 | use skulpin_renderer::rafx::api::RafxError; 18 | use crate::rafx::api::RafxExtents2D; 19 | 20 | /// Represents an error from creating the renderer 21 | #[derive(Debug)] 22 | pub enum AppError { 23 | RafxError(skulpin_renderer::rafx::api::RafxError), 24 | WinitError(winit::error::OsError), 25 | } 26 | 27 | impl std::error::Error for AppError { 28 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 29 | match *self { 30 | AppError::RafxError(ref e) => Some(e), 31 | AppError::WinitError(ref e) => Some(e), 32 | } 33 | } 34 | } 35 | 36 | impl core::fmt::Display for AppError { 37 | fn fmt( 38 | &self, 39 | fmt: &mut core::fmt::Formatter, 40 | ) -> core::fmt::Result { 41 | match *self { 42 | AppError::RafxError(ref e) => e.fmt(fmt), 43 | AppError::WinitError(ref e) => e.fmt(fmt), 44 | } 45 | } 46 | } 47 | 48 | impl From for AppError { 49 | fn from(result: RafxError) -> Self { 50 | AppError::RafxError(result) 51 | } 52 | } 53 | 54 | impl From for AppError { 55 | fn from(result: winit::error::OsError) -> Self { 56 | AppError::WinitError(result) 57 | } 58 | } 59 | 60 | pub struct AppUpdateArgs<'a, 'b, 'c> { 61 | pub app_control: &'a mut AppControl, 62 | pub input_state: &'b InputState, 63 | pub time_state: &'c TimeState, 64 | } 65 | 66 | pub struct AppDrawArgs<'a, 'b, 'c, 'd> { 67 | pub app_control: &'a AppControl, 68 | pub input_state: &'b InputState, 69 | pub time_state: &'c TimeState, 70 | pub canvas: &'d mut skia_safe::Canvas, 71 | pub coordinate_system_helper: CoordinateSystemHelper, 72 | } 73 | 74 | /// A skulpin app requires implementing the AppHandler. A separate update and draw call must be 75 | /// implemented. 76 | /// 77 | /// `update` is called when winit provides a `winit::event::Event::MainEventsCleared` message 78 | /// 79 | /// `draw` is called when winit provides a `winit::event::RedrawRequested` message 80 | /// 81 | /// I would recommend putting general logic you always want to run in the `update` and just 82 | /// rendering code in the `draw`. 83 | pub trait AppHandler { 84 | /// Called frequently, this is the intended place to put non-rendering logic 85 | fn update( 86 | &mut self, 87 | update_args: AppUpdateArgs, 88 | ); 89 | 90 | /// Called frequently, this is the intended place to put drawing code 91 | fn draw( 92 | &mut self, 93 | draw_args: AppDrawArgs, 94 | ); 95 | 96 | fn fatal_error( 97 | &mut self, 98 | error: &AppError, 99 | ); 100 | } 101 | 102 | /// Used to configure the app behavior and create the app 103 | pub struct AppBuilder { 104 | inner_size: Size, 105 | window_title: String, 106 | renderer_builder: RendererBuilder, 107 | } 108 | 109 | impl Default for AppBuilder { 110 | fn default() -> Self { 111 | AppBuilder::new() 112 | } 113 | } 114 | 115 | impl AppBuilder { 116 | /// Construct the app builder initialized with default options 117 | pub fn new() -> Self { 118 | AppBuilder { 119 | inner_size: LogicalSize::new(900, 600).into(), 120 | window_title: "Skulpin".to_string(), 121 | renderer_builder: RendererBuilder::new(), 122 | } 123 | } 124 | 125 | /// Specifies the inner size of the window. Both physical and logical coordinates are accepted. 126 | pub fn inner_size>( 127 | mut self, 128 | inner_size: S, 129 | ) -> Self { 130 | self.inner_size = inner_size.into(); 131 | self 132 | } 133 | 134 | /// Specifies the title that the window will be created with 135 | pub fn window_title>( 136 | mut self, 137 | window_title: T, 138 | ) -> Self { 139 | self.window_title = window_title.into(); 140 | self 141 | } 142 | 143 | /// Determine the coordinate system to use for the canvas. This can be overridden by using the 144 | /// canvas sizer passed into the draw callback 145 | pub fn coordinate_system( 146 | mut self, 147 | coordinate_system: CoordinateSystem, 148 | ) -> Self { 149 | self.renderer_builder = self.renderer_builder.coordinate_system(coordinate_system); 150 | self 151 | } 152 | 153 | /// Set the validation mode in rafx. For skulpin, this essentially means turning the vulkan 154 | /// debug layers on/off. 155 | pub fn validation_mode( 156 | mut self, 157 | validation_mode: ValidationMode, 158 | ) -> Self { 159 | self.renderer_builder = self.renderer_builder.validation_mode(validation_mode); 160 | self 161 | } 162 | 163 | /// Start the app. `app_handler` must be an implementation of [skulpin::app::AppHandler]. 164 | /// This does not return because winit does not return. For consistency, we use the 165 | /// fatal_error() callback on the passed in AppHandler. 166 | pub fn run( 167 | self, 168 | app_handler: T, 169 | ) -> ! { 170 | App::run( 171 | app_handler, 172 | self.inner_size, 173 | self.window_title.clone(), 174 | self.renderer_builder, 175 | ) 176 | } 177 | } 178 | 179 | /// Constructed by `AppBuilder` which immediately calls `run`. 180 | pub struct App {} 181 | 182 | impl App { 183 | /// Runs the app. This is called by `AppBuilder::run`. This does not return because winit does 184 | /// not return. For consistency, we use the fatal_error() callback on the passed in AppHandler. 185 | pub fn run( 186 | mut app_handler: T, 187 | inner_size: Size, 188 | window_title: String, 189 | renderer_builder: RendererBuilder, 190 | ) -> ! { 191 | // Create the event loop 192 | let event_loop = winit::event_loop::EventLoop::<()>::with_user_event(); 193 | 194 | let winit_size = match inner_size { 195 | Size::Physical(physical_size) => winit::dpi::Size::Physical( 196 | winit::dpi::PhysicalSize::new(physical_size.width, physical_size.height), 197 | ), 198 | Size::Logical(logical_size) => winit::dpi::Size::Logical(winit::dpi::LogicalSize::new( 199 | logical_size.width as f64, 200 | logical_size.height as f64, 201 | )), 202 | }; 203 | 204 | // Create a single window 205 | let window_result = winit::window::WindowBuilder::new() 206 | .with_title(window_title) 207 | .with_inner_size(winit_size) 208 | .build(&event_loop); 209 | 210 | let window = match window_result { 211 | Ok(window) => window, 212 | Err(e) => { 213 | warn!("Passing WindowBuilder::build() error to app {}", e); 214 | 215 | let app_error = e.into(); 216 | app_handler.fatal_error(&app_error); 217 | 218 | // Exiting in this way is consistent with how we will exit if we fail within the 219 | // input loop 220 | std::process::exit(0); 221 | } 222 | }; 223 | 224 | let mut app_control = AppControl::default(); 225 | let mut time_state = TimeState::new(); 226 | let mut input_state = InputState::new(&window); 227 | 228 | let window_size = window.inner_size(); 229 | let window_extents = RafxExtents2D { 230 | width: window_size.width, 231 | height: window_size.height, 232 | }; 233 | 234 | let renderer_result = renderer_builder.build(&window, window_extents); 235 | let mut renderer = match renderer_result { 236 | Ok(renderer) => renderer, 237 | Err(e) => { 238 | warn!("Passing RendererBuilder::build() error to app {}", e); 239 | 240 | let app_error = e.into(); 241 | app_handler.fatal_error(&app_error); 242 | 243 | // Exiting in this way is consistent with how we will exit if we fail within the 244 | // input loop 245 | std::process::exit(0); 246 | } 247 | }; 248 | 249 | // To print fps once per second 250 | let mut print_fps_event = PeriodicEvent::default(); 251 | 252 | // Pass control of this thread to winit until the app terminates. If this app wants to quit, 253 | // the update loop should send the appropriate event via the channel 254 | event_loop.run(move |event, window_target, control_flow| { 255 | input_state.handle_winit_event(&mut app_control, &event, window_target); 256 | 257 | match event { 258 | winit::event::Event::MainEventsCleared => { 259 | time_state.update(); 260 | 261 | if print_fps_event.try_take_event( 262 | time_state.current_instant(), 263 | std::time::Duration::from_secs(1), 264 | ) { 265 | debug!("fps: {}", time_state.updates_per_second()); 266 | } 267 | 268 | app_handler.update(AppUpdateArgs { 269 | app_control: &mut app_control, 270 | input_state: &input_state, 271 | time_state: &time_state, 272 | }); 273 | 274 | // Call this to mark the start of the next frame (i.e. "key just down" will return false) 275 | input_state.end_frame(); 276 | 277 | // Queue a RedrawRequested event. 278 | window.request_redraw(); 279 | } 280 | winit::event::Event::RedrawRequested(_window_id) => { 281 | let window_size = window.inner_size(); 282 | let window_extents = RafxExtents2D { 283 | width: window_size.width, 284 | height: window_size.height, 285 | }; 286 | 287 | if let Err(e) = renderer.draw( 288 | window_extents, 289 | window.scale_factor(), 290 | |canvas, coordinate_system_helper| { 291 | app_handler.draw(AppDrawArgs { 292 | app_control: &app_control, 293 | input_state: &input_state, 294 | time_state: &time_state, 295 | canvas, 296 | coordinate_system_helper, 297 | }); 298 | }, 299 | ) { 300 | warn!("Passing Renderer::draw() error to app {}", e); 301 | app_handler.fatal_error(&e.into()); 302 | app_control.enqueue_terminate_process(); 303 | } 304 | } 305 | _ => {} 306 | } 307 | 308 | if app_control.should_terminate_process() { 309 | *control_flow = winit::event_loop::ControlFlow::Exit 310 | } 311 | }); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /skulpin-app-winit/src/app_control.rs: -------------------------------------------------------------------------------- 1 | //! Serves as the interface for an app implementation to affect the behavior of the app that's 2 | //! hosting it 3 | 4 | /// State that drives high-level decision making for the app 5 | #[derive(Default)] 6 | pub struct AppControl { 7 | /// If true, the application will quit when the next frame ends 8 | should_terminate_process: bool, 9 | } 10 | 11 | impl AppControl { 12 | /// Direct the application to terminate at the end of the next frame 13 | pub fn enqueue_terminate_process(&mut self) { 14 | self.should_terminate_process = true; 15 | } 16 | 17 | /// Returns true iff `enqueue_terminate_process` is called, indicating that the app should terminate 18 | pub fn should_terminate_process(&self) -> bool { 19 | self.should_terminate_process 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /skulpin-app-winit/src/input_state.rs: -------------------------------------------------------------------------------- 1 | //! Handles input tracking and provides an easy way to detect clicks, dragging, etc. 2 | 3 | use crate::winit; 4 | 5 | // Re-export winit types 6 | pub use winit::event::VirtualKeyCode; 7 | pub use winit::event::MouseButton; 8 | pub use winit::event::MouseScrollDelta; 9 | pub use winit::event::ElementState; 10 | pub use winit::dpi::LogicalSize; 11 | pub use winit::dpi::PhysicalSize; 12 | pub use winit::dpi::LogicalPosition; 13 | pub use winit::dpi::PhysicalPosition; 14 | pub use winit::dpi::Size; 15 | pub use winit::dpi::Position; 16 | 17 | use super::AppControl; 18 | use crate::winit::window::Window; 19 | 20 | /// Encapsulates the state of a mouse drag 21 | #[derive(Copy, Clone, Debug)] 22 | pub struct MouseDragState { 23 | /// Logical position where the drag began 24 | pub begin_position: PhysicalPosition, 25 | 26 | /// Logical position where the drag ended 27 | pub end_position: PhysicalPosition, 28 | 29 | /// Amount of mouse movement in the previous frame 30 | pub previous_frame_delta: PhysicalPosition, 31 | 32 | /// Amount of mouse movement in total 33 | pub accumulated_frame_delta: PhysicalPosition, 34 | } 35 | 36 | /// State of input devices. This is maintained by processing events from winit 37 | pub struct InputState { 38 | window_size: PhysicalSize, 39 | scale_factor: f64, 40 | 41 | key_is_down: [bool; Self::KEYBOARD_BUTTON_COUNT], 42 | key_just_down: [bool; Self::KEYBOARD_BUTTON_COUNT], 43 | key_just_up: [bool; Self::KEYBOARD_BUTTON_COUNT], 44 | 45 | mouse_position: PhysicalPosition, 46 | mouse_wheel_delta: MouseScrollDelta, 47 | mouse_button_is_down: [bool; Self::MOUSE_BUTTON_COUNT], 48 | mouse_button_just_down: [Option>; Self::MOUSE_BUTTON_COUNT], 49 | mouse_button_just_up: [Option>; Self::MOUSE_BUTTON_COUNT], 50 | 51 | mouse_button_just_clicked: [Option>; Self::MOUSE_BUTTON_COUNT], 52 | 53 | mouse_button_went_down_position: [Option>; Self::MOUSE_BUTTON_COUNT], 54 | mouse_button_went_up_position: [Option>; Self::MOUSE_BUTTON_COUNT], 55 | 56 | mouse_drag_in_progress: [Option; Self::MOUSE_BUTTON_COUNT], 57 | mouse_drag_just_finished: [Option; Self::MOUSE_BUTTON_COUNT], 58 | } 59 | 60 | impl InputState { 61 | /// Number of keyboard buttons we will track. Any button with a higher virtual key code will be 62 | /// ignored 63 | pub const KEYBOARD_BUTTON_COUNT: usize = 255; 64 | 65 | /// Number of mouse buttons we will track. Any button with a higher index will be ignored. 66 | pub const MOUSE_BUTTON_COUNT: usize = 7; 67 | 68 | /// Distance in LogicalPosition units that the mouse has to be dragged to be considered a drag 69 | /// rather than a click 70 | const MIN_DRAG_DISTANCE: f64 = 2.0; 71 | } 72 | 73 | impl InputState { 74 | /// Create a new input state to track the given window 75 | pub fn new(window: &Window) -> InputState { 76 | InputState { 77 | window_size: window.inner_size(), 78 | scale_factor: window.scale_factor(), 79 | key_is_down: [false; Self::KEYBOARD_BUTTON_COUNT], 80 | key_just_down: [false; Self::KEYBOARD_BUTTON_COUNT], 81 | key_just_up: [false; Self::KEYBOARD_BUTTON_COUNT], 82 | mouse_position: PhysicalPosition::new(0.0, 0.0), 83 | mouse_wheel_delta: MouseScrollDelta::LineDelta(0.0, 0.0), 84 | mouse_button_is_down: [false; Self::MOUSE_BUTTON_COUNT], 85 | mouse_button_just_down: [None; Self::MOUSE_BUTTON_COUNT], 86 | mouse_button_just_up: [None; Self::MOUSE_BUTTON_COUNT], 87 | mouse_button_just_clicked: [None; Self::MOUSE_BUTTON_COUNT], 88 | mouse_button_went_down_position: [None; Self::MOUSE_BUTTON_COUNT], 89 | mouse_button_went_up_position: [None; Self::MOUSE_BUTTON_COUNT], 90 | mouse_drag_in_progress: [None; Self::MOUSE_BUTTON_COUNT], 91 | mouse_drag_just_finished: [None; Self::MOUSE_BUTTON_COUNT], 92 | } 93 | } 94 | 95 | // 96 | // Accessors 97 | // 98 | 99 | /// Current size of window 100 | pub fn window_size(&self) -> PhysicalSize { 101 | self.window_size 102 | } 103 | 104 | /// The scaling factor due to high-dpi screens 105 | pub fn scale_factor(&self) -> f64 { 106 | self.scale_factor 107 | } 108 | 109 | /// Returns true if the given key is down 110 | pub fn is_key_down( 111 | &self, 112 | key: VirtualKeyCode, 113 | ) -> bool { 114 | if let Some(index) = Self::keyboard_button_to_index(key) { 115 | self.key_is_down[index] 116 | } else { 117 | false 118 | } 119 | } 120 | 121 | /// Returns true if the key went down during this frame 122 | pub fn is_key_just_down( 123 | &self, 124 | key: VirtualKeyCode, 125 | ) -> bool { 126 | if let Some(index) = Self::keyboard_button_to_index(key) { 127 | self.key_just_down[index] 128 | } else { 129 | false 130 | } 131 | } 132 | 133 | /// Returns true if the key went up during this frame 134 | pub fn is_key_just_up( 135 | &self, 136 | key: VirtualKeyCode, 137 | ) -> bool { 138 | if let Some(index) = Self::keyboard_button_to_index(key) { 139 | self.key_just_up[index] 140 | } else { 141 | false 142 | } 143 | } 144 | 145 | /// Get the current mouse position 146 | pub fn mouse_position(&self) -> PhysicalPosition { 147 | self.mouse_position 148 | } 149 | 150 | /// Get the scroll delta from the current frame 151 | pub fn mouse_wheel_delta(&self) -> MouseScrollDelta { 152 | self.mouse_wheel_delta 153 | } 154 | 155 | /// Returns true if the given button is down 156 | pub fn is_mouse_down( 157 | &self, 158 | mouse_button: MouseButton, 159 | ) -> bool { 160 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 161 | self.mouse_button_is_down[index] 162 | } else { 163 | false 164 | } 165 | } 166 | 167 | /// Returns true if the button went down during this frame 168 | pub fn is_mouse_just_down( 169 | &self, 170 | mouse_button: MouseButton, 171 | ) -> bool { 172 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 173 | self.mouse_button_just_down[index].is_some() 174 | } else { 175 | false 176 | } 177 | } 178 | 179 | /// Returns the position the mouse just went down at, otherwise returns None 180 | pub fn mouse_just_down_position( 181 | &self, 182 | mouse_button: MouseButton, 183 | ) -> Option> { 184 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 185 | self.mouse_button_just_down[index] 186 | } else { 187 | None 188 | } 189 | } 190 | 191 | /// Returns true if the button went up during this frame 192 | pub fn is_mouse_just_up( 193 | &self, 194 | mouse_button: MouseButton, 195 | ) -> bool { 196 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 197 | self.mouse_button_just_up[index].is_some() 198 | } else { 199 | false 200 | } 201 | } 202 | 203 | /// Returns the position the mouse just went up at, otherwise returns None 204 | pub fn mouse_just_up_position( 205 | &self, 206 | mouse_button: MouseButton, 207 | ) -> Option> { 208 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 209 | self.mouse_button_just_up[index] 210 | } else { 211 | None 212 | } 213 | } 214 | 215 | /// Returns true if the button was just clicked. "Clicked" means the button went down and came 216 | /// back up without being moved much. If it was moved, it would be considered a drag. 217 | pub fn is_mouse_button_just_clicked( 218 | &self, 219 | mouse_button: MouseButton, 220 | ) -> bool { 221 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 222 | self.mouse_button_just_clicked[index].is_some() 223 | } else { 224 | false 225 | } 226 | } 227 | 228 | /// Returns the position the button was just clicked at, otherwise None. "Clicked" means the 229 | /// button went down and came back up without being moved much. If it was moved, it would be 230 | /// considered a drag. 231 | pub fn mouse_button_just_clicked_position( 232 | &self, 233 | mouse_button: MouseButton, 234 | ) -> Option> { 235 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 236 | self.mouse_button_just_clicked[index] 237 | } else { 238 | None 239 | } 240 | } 241 | 242 | /// Returns the position the button went down at previously. This could have been some time ago. 243 | pub fn mouse_button_went_down_position( 244 | &self, 245 | mouse_button: MouseButton, 246 | ) -> Option> { 247 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 248 | self.mouse_button_went_down_position[index] 249 | } else { 250 | None 251 | } 252 | } 253 | 254 | /// Returns the position the button went up at previously. This could have been some time ago. 255 | pub fn mouse_button_went_up_position( 256 | &self, 257 | mouse_button: MouseButton, 258 | ) -> Option> { 259 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 260 | self.mouse_button_went_up_position[index] 261 | } else { 262 | None 263 | } 264 | } 265 | 266 | /// Return true if the mouse is being dragged. (A drag means the button went down and mouse 267 | /// moved, but button hasn't come back up yet) 268 | pub fn is_mouse_drag_in_progress( 269 | &self, 270 | mouse_button: MouseButton, 271 | ) -> bool { 272 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 273 | self.mouse_drag_in_progress[index].is_some() 274 | } else { 275 | false 276 | } 277 | } 278 | 279 | /// Returns the mouse drag state if a drag is in process, otherwise None. 280 | pub fn mouse_drag_in_progress( 281 | &self, 282 | mouse_button: MouseButton, 283 | ) -> Option { 284 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 285 | self.mouse_drag_in_progress[index] 286 | } else { 287 | None 288 | } 289 | } 290 | 291 | /// Return true if a mouse drag completed in the previous frame, otherwise false 292 | pub fn is_mouse_drag_just_finished( 293 | &self, 294 | mouse_button: MouseButton, 295 | ) -> bool { 296 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 297 | self.mouse_drag_just_finished[index].is_some() 298 | } else { 299 | false 300 | } 301 | } 302 | 303 | /// Returns information about a mouse drag if it just completed, otherwise None 304 | pub fn mouse_drag_just_finished( 305 | &self, 306 | mouse_button: MouseButton, 307 | ) -> Option { 308 | if let Some(index) = Self::mouse_button_to_index(mouse_button) { 309 | self.mouse_drag_just_finished[index] 310 | } else { 311 | None 312 | } 313 | } 314 | 315 | // 316 | // Handlers for significant events 317 | // 318 | 319 | /// Call at the end of every frame. This clears events that were "just" completed. 320 | pub fn end_frame(&mut self) { 321 | self.mouse_wheel_delta = MouseScrollDelta::LineDelta(0.0, 0.0); 322 | 323 | for value in self.key_just_down.iter_mut() { 324 | *value = false; 325 | } 326 | 327 | for value in self.key_just_up.iter_mut() { 328 | *value = false; 329 | } 330 | 331 | for value in self.mouse_button_just_down.iter_mut() { 332 | *value = None; 333 | } 334 | 335 | for value in self.mouse_button_just_up.iter_mut() { 336 | *value = None; 337 | } 338 | 339 | for value in self.mouse_button_just_clicked.iter_mut() { 340 | *value = None; 341 | } 342 | 343 | for value in self.mouse_drag_just_finished.iter_mut() { 344 | *value = None; 345 | } 346 | 347 | for value in self.mouse_drag_in_progress.iter_mut() { 348 | if let Some(v) = value { 349 | v.previous_frame_delta = PhysicalPosition::new(0.0, 0.0); 350 | } 351 | } 352 | } 353 | 354 | /// Call when DPI factor changes 355 | fn handle_scale_factor_changed( 356 | &mut self, 357 | scale_factor: f64, 358 | ) { 359 | self.scale_factor = scale_factor; 360 | } 361 | 362 | /// Call when window size changes 363 | fn handle_window_size_changed( 364 | &mut self, 365 | window_size: PhysicalSize, 366 | ) { 367 | self.window_size = window_size; 368 | } 369 | 370 | /// Call when a key event occurs 371 | fn handle_keyboard_event( 372 | &mut self, 373 | keyboard_button: VirtualKeyCode, 374 | button_state: ElementState, 375 | ) { 376 | if let Some(kc) = Self::keyboard_button_to_index(keyboard_button) { 377 | // Assign true if key is down, or false if key is up 378 | if button_state == ElementState::Pressed { 379 | if !self.key_is_down[kc] { 380 | self.key_just_down[kc] = true; 381 | } 382 | self.key_is_down[kc] = true 383 | } else { 384 | if self.key_is_down[kc] { 385 | self.key_just_up[kc] = true; 386 | } 387 | self.key_is_down[kc] = false 388 | } 389 | } 390 | } 391 | 392 | /// Call when a mouse button event occurs 393 | fn handle_mouse_button_event( 394 | &mut self, 395 | button: MouseButton, 396 | button_event: ElementState, 397 | ) { 398 | if let Some(button_index) = Self::mouse_button_to_index(button) { 399 | assert!(button_index < InputState::MOUSE_BUTTON_COUNT); 400 | 401 | // Update is down/up, just down/up 402 | match button_event { 403 | ElementState::Pressed => { 404 | self.mouse_button_just_down[button_index] = Some(self.mouse_position); 405 | self.mouse_button_is_down[button_index] = true; 406 | 407 | self.mouse_button_went_down_position[button_index] = Some(self.mouse_position); 408 | } 409 | ElementState::Released => { 410 | self.mouse_button_just_up[button_index] = Some(self.mouse_position); 411 | self.mouse_button_is_down[button_index] = false; 412 | 413 | self.mouse_button_went_up_position[button_index] = Some(self.mouse_position); 414 | 415 | match self.mouse_drag_in_progress[button_index] { 416 | Some(in_progress) => { 417 | let delta = Self::subtract_physical( 418 | self.mouse_position, 419 | Self::add_physical( 420 | in_progress.begin_position, 421 | in_progress.accumulated_frame_delta, 422 | ), 423 | ); 424 | self.mouse_drag_just_finished[button_index] = Some(MouseDragState { 425 | begin_position: in_progress.begin_position, 426 | end_position: self.mouse_position, 427 | previous_frame_delta: delta, 428 | accumulated_frame_delta: Self::add_physical( 429 | in_progress.accumulated_frame_delta, 430 | delta, 431 | ), 432 | }); 433 | } 434 | None => { 435 | self.mouse_button_just_clicked[button_index] = Some(self.mouse_position) 436 | } 437 | } 438 | 439 | self.mouse_drag_in_progress[button_index] = None; 440 | } 441 | } 442 | } 443 | } 444 | 445 | /// Call when a mouse move occurs 446 | fn handle_mouse_move_event( 447 | &mut self, 448 | position: PhysicalPosition, 449 | ) { 450 | //let old_mouse_position = self.mouse_position; 451 | 452 | // Update mouse position 453 | self.mouse_position = position; 454 | 455 | // Update drag in progress state 456 | for i in 0..Self::MOUSE_BUTTON_COUNT { 457 | if self.mouse_button_is_down[i] { 458 | self.mouse_drag_in_progress[i] = match self.mouse_drag_in_progress[i] { 459 | None => { 460 | match self.mouse_button_went_down_position[i] { 461 | Some(went_down_position) => { 462 | let min_drag_distance_met = Self::distance_physical( 463 | went_down_position, 464 | self.mouse_position, 465 | ) > Self::MIN_DRAG_DISTANCE; 466 | if min_drag_distance_met { 467 | // We dragged a non-trivial amount, start the drag 468 | Some(MouseDragState { 469 | begin_position: went_down_position, 470 | end_position: self.mouse_position, 471 | previous_frame_delta: Self::subtract_physical( 472 | self.mouse_position, 473 | went_down_position, 474 | ), 475 | accumulated_frame_delta: Self::subtract_physical( 476 | self.mouse_position, 477 | went_down_position, 478 | ), 479 | }) 480 | } else { 481 | // Mouse moved too small an amount to be considered a drag 482 | None 483 | } 484 | } 485 | 486 | // We don't know where the mosue went down, so we can't start a drag 487 | None => None, 488 | } 489 | } 490 | Some(old_drag_state) => { 491 | // We were already dragging, so just update the end position 492 | 493 | let delta = Self::subtract_physical( 494 | self.mouse_position, 495 | Self::add_physical( 496 | old_drag_state.begin_position, 497 | old_drag_state.accumulated_frame_delta, 498 | ), 499 | ); 500 | Some(MouseDragState { 501 | begin_position: old_drag_state.begin_position, 502 | end_position: self.mouse_position, 503 | previous_frame_delta: delta, 504 | accumulated_frame_delta: Self::add_physical( 505 | old_drag_state.accumulated_frame_delta, 506 | delta, 507 | ), 508 | }) 509 | } 510 | }; 511 | } 512 | } 513 | } 514 | 515 | fn handle_mouse_wheel_event( 516 | &mut self, 517 | delta: MouseScrollDelta, 518 | ) { 519 | // Try to add the delta to self.mouse_wheel_delta 520 | if let MouseScrollDelta::LineDelta(x1, y1) = self.mouse_wheel_delta { 521 | if let MouseScrollDelta::LineDelta(x2, y2) = delta { 522 | self.mouse_wheel_delta = MouseScrollDelta::LineDelta(x1 + x2, y1 + y2); 523 | } else { 524 | self.mouse_wheel_delta = delta; 525 | } 526 | } else if let MouseScrollDelta::PixelDelta(d1) = self.mouse_wheel_delta { 527 | if let MouseScrollDelta::PixelDelta(d2) = delta { 528 | #[cfg(any(feature = "winit-21", feature = "winit-22"))] 529 | { 530 | self.mouse_wheel_delta = MouseScrollDelta::PixelDelta( 531 | LogicalPosition::::new(d1.x + d2.x, d1.y + d2.y), 532 | ); 533 | } 534 | 535 | #[cfg(any( 536 | feature = "winit-23", 537 | feature = "winit-24", 538 | feature = "winit-25", 539 | feature = "winit-latest" 540 | ))] 541 | { 542 | self.mouse_wheel_delta = MouseScrollDelta::PixelDelta( 543 | PhysicalPosition::::new(d1.x + d2.x, d1.y + d2.y), 544 | ); 545 | } 546 | } else { 547 | self.mouse_wheel_delta = delta; 548 | } 549 | } 550 | 551 | self.mouse_wheel_delta = delta; 552 | } 553 | 554 | /// Call when winit sends an event 555 | pub fn handle_winit_event( 556 | &mut self, 557 | app_control: &mut AppControl, 558 | event: &winit::event::Event, 559 | _window_target: &winit::event_loop::EventLoopWindowTarget, 560 | ) { 561 | use crate::winit::event::Event; 562 | use crate::winit::event::WindowEvent; 563 | 564 | let mut is_close_requested = false; 565 | 566 | match event { 567 | // Close if the window is killed 568 | Event::WindowEvent { 569 | event: WindowEvent::CloseRequested, 570 | .. 571 | } => is_close_requested = true, 572 | 573 | Event::WindowEvent { 574 | event: 575 | WindowEvent::ScaleFactorChanged { 576 | scale_factor, 577 | new_inner_size, 578 | }, 579 | .. 580 | } => { 581 | trace!("dpi scaling factor changed {:?}", scale_factor); 582 | self.handle_scale_factor_changed(*scale_factor); 583 | self.handle_window_size_changed(**new_inner_size); 584 | } 585 | 586 | Event::WindowEvent { 587 | event: WindowEvent::Resized(window_size), 588 | .. 589 | } => self.handle_window_size_changed(*window_size), 590 | 591 | //Process keyboard input 592 | Event::WindowEvent { 593 | event: WindowEvent::KeyboardInput { input, .. }, 594 | .. 595 | } => { 596 | trace!("keyboard input {:?}", input); 597 | if let Some(vk) = input.virtual_keycode { 598 | self.handle_keyboard_event(vk, input.state); 599 | } 600 | } 601 | 602 | Event::WindowEvent { 603 | event: 604 | WindowEvent::MouseInput { 605 | device_id, 606 | state, 607 | button, 608 | .. 609 | }, 610 | .. 611 | } => { 612 | trace!( 613 | "mouse button input {:?} {:?} {:?}", 614 | device_id, 615 | state, 616 | button, 617 | ); 618 | 619 | self.handle_mouse_button_event(*button, *state); 620 | } 621 | 622 | Event::WindowEvent { 623 | event: 624 | WindowEvent::CursorMoved { 625 | device_id, 626 | position, 627 | .. 628 | }, 629 | .. 630 | } => { 631 | trace!("mouse move input {:?} {:?}", device_id, position,); 632 | self.handle_mouse_move_event(*position); 633 | } 634 | 635 | Event::WindowEvent { 636 | event: 637 | WindowEvent::MouseWheel { 638 | device_id, delta, .. 639 | }, 640 | .. 641 | } => { 642 | trace!("mouse wheel {:?} {:?}", device_id, delta); 643 | self.handle_mouse_wheel_event(*delta); 644 | } 645 | 646 | // Ignore any other events 647 | _ => (), 648 | } 649 | 650 | if is_close_requested { 651 | trace!("close requested"); 652 | app_control.enqueue_terminate_process(); 653 | } 654 | } 655 | 656 | // 657 | // Helper functions 658 | // 659 | 660 | /// Convert the winit mouse button enum into a numerical index 661 | pub fn mouse_button_to_index(button: MouseButton) -> Option { 662 | let index = match button { 663 | MouseButton::Left => 0, 664 | MouseButton::Right => 1, 665 | MouseButton::Middle => 2, 666 | MouseButton::Other(x) => (x as usize) + 3, 667 | }; 668 | 669 | if index >= Self::MOUSE_BUTTON_COUNT { 670 | None 671 | } else { 672 | Some(index) 673 | } 674 | } 675 | 676 | /// Convert to the winit mouse button enum from a numerical index 677 | pub fn mouse_index_to_button(index: usize) -> Option { 678 | if index >= Self::MOUSE_BUTTON_COUNT { 679 | None 680 | } else { 681 | let button = match index { 682 | 0 => MouseButton::Left, 683 | 1 => MouseButton::Right, 684 | 2 => MouseButton::Middle, 685 | _ => MouseButton::Other((index - 3) as _), 686 | }; 687 | 688 | Some(button) 689 | } 690 | } 691 | 692 | /// Convert the winit virtual key code into a numerical index 693 | pub fn keyboard_button_to_index(button: VirtualKeyCode) -> Option { 694 | let index = button as usize; 695 | if index >= Self::KEYBOARD_BUTTON_COUNT { 696 | None 697 | } else { 698 | Some(index) 699 | } 700 | } 701 | 702 | /// Adds two logical positions (p0 + p1) 703 | fn add_physical( 704 | p0: PhysicalPosition, 705 | p1: PhysicalPosition, 706 | ) -> PhysicalPosition { 707 | PhysicalPosition::new(p0.x + p1.x, p0.y + p1.y) 708 | } 709 | 710 | /// Subtracts two logical positions (p0 - p1) 711 | fn subtract_physical( 712 | p0: PhysicalPosition, 713 | p1: PhysicalPosition, 714 | ) -> PhysicalPosition { 715 | PhysicalPosition::new(p0.x - p1.x, p0.y - p1.y) 716 | } 717 | 718 | /// Gets the distance between two logical positions 719 | fn distance_physical( 720 | p0: PhysicalPosition, 721 | p1: PhysicalPosition, 722 | ) -> f64 { 723 | let x_diff = (p1.x - p0.x) as f64; 724 | let y_diff = (p1.y - p0.y) as f64; 725 | 726 | ((x_diff * x_diff) + (y_diff * y_diff)).sqrt() 727 | } 728 | } 729 | -------------------------------------------------------------------------------- /skulpin-app-winit/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | mod app; 5 | pub use app::App; 6 | pub use app::AppHandler; 7 | pub use app::AppBuilder; 8 | pub use app::AppError; 9 | pub use app::AppUpdateArgs; 10 | pub use app::AppDrawArgs; 11 | 12 | mod app_control; 13 | pub use app_control::AppControl; 14 | 15 | mod input_state; 16 | pub use input_state::InputState; 17 | pub use input_state::MouseDragState; 18 | 19 | // These are re-exported winit types 20 | pub use input_state::VirtualKeyCode; 21 | pub use input_state::MouseButton; 22 | pub use input_state::MouseScrollDelta; 23 | pub use input_state::ElementState; 24 | pub use input_state::LogicalSize; 25 | pub use input_state::PhysicalSize; 26 | pub use input_state::LogicalPosition; 27 | pub use input_state::PhysicalPosition; 28 | pub use input_state::Position; 29 | pub use input_state::Size; 30 | 31 | mod time_state; 32 | pub use time_state::TimeState; 33 | pub use time_state::TimeContext; 34 | 35 | mod util; 36 | pub use util::PeriodicEvent; 37 | pub use util::ScopeTimer; 38 | 39 | pub use skulpin_renderer::rafx; 40 | pub use skulpin_renderer::skia_safe; 41 | 42 | #[cfg(feature = "winit-21")] 43 | pub use winit_21 as winit; 44 | #[cfg(feature = "winit-22")] 45 | pub use winit_22 as winit; 46 | #[cfg(feature = "winit-23")] 47 | pub use winit_23 as winit; 48 | #[cfg(feature = "winit-24")] 49 | pub use winit_24 as winit; 50 | #[cfg(feature = "winit-25")] 51 | pub use winit_25 as winit; 52 | #[cfg(feature = "winit-latest")] 53 | pub use winit_latest as winit; 54 | -------------------------------------------------------------------------------- /skulpin-app-winit/src/time_state.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for tracking time in a skulpin App 2 | 3 | use std::time; 4 | 5 | const NANOS_PER_SEC: u32 = 1_000_000_000; 6 | 7 | /// Contains the global time information (such as time when app was started.) There is also a 8 | /// time context that is continuously updated 9 | pub struct TimeState { 10 | app_start_system_time: time::SystemTime, 11 | app_start_instant: time::Instant, 12 | 13 | // Save the instant captured during previous update 14 | previous_update_instant: time::Instant, 15 | 16 | // This contains each context that we support. This will likely be removed in a future version 17 | // of skulpin 18 | app_time_context: TimeContext, 19 | } 20 | 21 | impl TimeState { 22 | /// Create a new TimeState. Default is not allowed because the current time affects the object 23 | #[allow(clippy::new_without_default)] 24 | pub fn new() -> TimeState { 25 | let now_instant = time::Instant::now(); 26 | let now_system_time = time::SystemTime::now(); 27 | 28 | TimeState { 29 | app_start_system_time: now_system_time, 30 | app_start_instant: now_instant, 31 | previous_update_instant: now_instant, 32 | app_time_context: TimeContext::new(), 33 | } 34 | } 35 | 36 | /// Call every frame to capture time passing and update values 37 | pub fn update(&mut self) { 38 | // Determine length of time since last tick 39 | let now_instant = time::Instant::now(); 40 | let elapsed = now_instant - self.previous_update_instant; 41 | self.previous_update_instant = now_instant; 42 | self.app_time_context.update(elapsed); 43 | } 44 | 45 | /// System time that the application started 46 | pub fn app_start_system_time(&self) -> &time::SystemTime { 47 | &self.app_start_system_time 48 | } 49 | 50 | /// rust Instant object captured when the application started 51 | pub fn app_start_instant(&self) -> &time::Instant { 52 | &self.app_start_instant 53 | } 54 | 55 | /// Get the app time context. 56 | pub fn app_time_context(&self) -> &TimeContext { 57 | &self.app_time_context 58 | } 59 | 60 | /// Duration of time passed 61 | pub fn total_time(&self) -> time::Duration { 62 | self.app_time_context.total_time 63 | } 64 | 65 | /// `std::time::Instant` object captured at the start of the most recent update 66 | pub fn current_instant(&self) -> time::Instant { 67 | self.app_time_context.current_instant 68 | } 69 | 70 | /// duration of time passed during the previous update 71 | pub fn previous_update_time(&self) -> time::Duration { 72 | self.app_time_context.previous_update_time 73 | } 74 | 75 | /// previous update time in f32 seconds 76 | pub fn previous_update_dt(&self) -> f32 { 77 | self.app_time_context.previous_update_dt 78 | } 79 | 80 | /// estimate of updates per second 81 | pub fn updates_per_second(&self) -> f32 { 82 | self.app_time_context.updates_per_second 83 | } 84 | 85 | /// estimate of updates per second smoothed over time 86 | pub fn updates_per_second_smoothed(&self) -> f32 { 87 | self.app_time_context.updates_per_second_smoothed 88 | } 89 | 90 | /// Total number of updates 91 | pub fn update_count(&self) -> u64 { 92 | self.app_time_context.update_count 93 | } 94 | } 95 | 96 | /// Tracks time passing, this is separate from the "global" `TimeState` since it would be 97 | /// possible to track a separate "context" of time, for example "unpaused" time in a game 98 | #[derive(Copy, Clone)] 99 | pub struct TimeContext { 100 | total_time: time::Duration, 101 | current_instant: time::Instant, 102 | previous_update_time: time::Duration, 103 | previous_update_dt: f32, 104 | updates_per_second: f32, 105 | updates_per_second_smoothed: f32, 106 | update_count: u64, 107 | } 108 | 109 | impl TimeContext { 110 | /// Create a new TimeState. Default is not allowed because the current time affects the object 111 | #[allow(clippy::new_without_default)] 112 | pub fn new() -> Self { 113 | let now_instant = time::Instant::now(); 114 | let zero_duration = time::Duration::from_secs(0); 115 | TimeContext { 116 | total_time: zero_duration, 117 | current_instant: now_instant, 118 | previous_update_time: zero_duration, 119 | previous_update_dt: 0.0, 120 | updates_per_second: 0.0, 121 | updates_per_second_smoothed: 0.0, 122 | update_count: 0, 123 | } 124 | } 125 | 126 | /// Call to capture time passing and update values 127 | pub fn update( 128 | &mut self, 129 | elapsed: std::time::Duration, 130 | ) { 131 | self.total_time += elapsed; 132 | self.current_instant += elapsed; 133 | self.previous_update_time = elapsed; 134 | 135 | // this can eventually be replaced with as_float_secs 136 | let dt = 137 | (elapsed.as_secs() as f32) + (elapsed.subsec_nanos() as f32) / (NANOS_PER_SEC as f32); 138 | 139 | self.previous_update_dt = dt; 140 | 141 | let fps = if dt > 0.0 { 1.0 / dt } else { 0.0 }; 142 | 143 | //TODO: Replace with a circular buffer 144 | const SMOOTHING_FACTOR: f32 = 0.95; 145 | self.updates_per_second = fps; 146 | self.updates_per_second_smoothed = (self.updates_per_second_smoothed * SMOOTHING_FACTOR) 147 | + (fps * (1.0 - SMOOTHING_FACTOR)); 148 | 149 | self.update_count += 1; 150 | } 151 | 152 | /// Duration of time passed in this time context 153 | pub fn total_time(&self) -> time::Duration { 154 | self.total_time 155 | } 156 | 157 | /// `std::time::Instant` object captured at the start of the most recent update in this time 158 | /// context 159 | pub fn current_instant(&self) -> time::Instant { 160 | self.current_instant 161 | } 162 | 163 | /// duration of time passed during the previous update 164 | pub fn previous_update_time(&self) -> time::Duration { 165 | self.previous_update_time 166 | } 167 | 168 | /// previous update time in f32 seconds 169 | pub fn previous_update_dt(&self) -> f32 { 170 | self.previous_update_dt 171 | } 172 | 173 | /// estimate of updates per second 174 | pub fn updates_per_second(&self) -> f32 { 175 | self.updates_per_second 176 | } 177 | 178 | /// estimate of updates per second smoothed over time 179 | pub fn updates_per_second_smoothed(&self) -> f32 { 180 | self.updates_per_second_smoothed 181 | } 182 | 183 | /// Total number of update in this time context 184 | pub fn update_count(&self) -> u64 { 185 | self.update_count 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /skulpin-app-winit/src/util.rs: -------------------------------------------------------------------------------- 1 | //! Handy utilities 2 | 3 | /// Records time when created and logs amount of time passed when dropped 4 | pub struct ScopeTimer<'a> { 5 | start_time: std::time::Instant, 6 | name: &'a str, 7 | } 8 | 9 | impl<'a> ScopeTimer<'a> { 10 | /// Records the current time. When dropped, the amount of time passed will be logged. 11 | #[allow(unused_must_use)] 12 | pub fn new(name: &'a str) -> Self { 13 | ScopeTimer { 14 | start_time: std::time::Instant::now(), 15 | name, 16 | } 17 | } 18 | } 19 | 20 | impl<'a> Drop for ScopeTimer<'a> { 21 | fn drop(&mut self) { 22 | let end_time = std::time::Instant::now(); 23 | trace!( 24 | "ScopeTimer {}: {}", 25 | self.name, 26 | (end_time - self.start_time).as_micros() as f64 / 1000.0 27 | ) 28 | } 29 | } 30 | 31 | /// Useful for cases where you want to do something once per time interval. 32 | #[derive(Default)] 33 | pub struct PeriodicEvent { 34 | last_time_triggered: Option, 35 | } 36 | 37 | impl PeriodicEvent { 38 | /// Call try_take_event to see if the required time has elapsed. It will return true only once 39 | /// enough time has passed since it last returned true. 40 | pub fn try_take_event( 41 | &mut self, 42 | current_time: std::time::Instant, 43 | wait_duration: std::time::Duration, 44 | ) -> bool { 45 | match self.last_time_triggered { 46 | None => { 47 | self.last_time_triggered = Some(current_time); 48 | true 49 | } 50 | Some(last_time_triggered) => { 51 | if current_time - last_time_triggered >= wait_duration { 52 | self.last_time_triggered = Some(current_time); 53 | true 54 | } else { 55 | false 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /skulpin-renderer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "skulpin-renderer" 3 | version = "0.14.1" 4 | authors = ["Philip Degarmo "] 5 | edition = "2018" 6 | description = "A vulkan renderer for skia, a component of skulpin" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/aclysma/skulpin" 9 | homepage = "https://github.com/aclysma/skulpin" 10 | keywords = ["skia", "vulkan", "ash", "2d", "graphics"] 11 | categories = ["graphics", "gui", "multimedia", "rendering", "visualization"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [features] 16 | # svg and shaper are deprecated. svg is always available, and shaper is included with textlayout. However, leaving them 17 | # since skia_safe prints informative error messages. Complete now just contains textlayout 18 | default = ["complete"] 19 | shaper = ["skia-safe/shaper"] 20 | svg = ["skia-safe/svg"] 21 | textlayout = ["skia-safe/textlayout"] 22 | complete = ["skia-safe/textlayout"] 23 | 24 | [dependencies] 25 | # rafx does not yet follow semver 26 | rafx = { version = "=0.0.14", features = ["rafx-vulkan", "framework"] } 27 | bincode = "1.3.1" 28 | lazy_static = "1" 29 | 30 | skia-safe = { version = "0.57.0", features = ["vulkan"] } 31 | skia-bindings = { version = "0.57.0" } 32 | 33 | log="0.4" 34 | -------------------------------------------------------------------------------- /skulpin-renderer/shaders/compile_shaders.bat: -------------------------------------------------------------------------------- 1 | rafx-shader-processor --glsl-path glsl/*.vert glsl/*.frag glsl/*.comp --cooked-shaders-path out --package-vk 2 | -------------------------------------------------------------------------------- /skulpin-renderer/shaders/compile_shaders.sh: -------------------------------------------------------------------------------- 1 | rafx-shader-processor --glsl-path glsl/*.vert glsl/*.frag glsl/*.comp --cooked-shaders-path out --package-vk 2 | -------------------------------------------------------------------------------- /skulpin-renderer/shaders/glsl/skia.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | #extension GL_ARB_separate_shader_objects : enable 3 | 4 | #include "skia.glsl" 5 | 6 | layout(location = 0) in vec2 uv; 7 | 8 | layout(location = 0) out vec4 outColor; 9 | 10 | void main() { 11 | outColor = texture(sampler2D(tex, smp), uv); 12 | } -------------------------------------------------------------------------------- /skulpin-renderer/shaders/glsl/skia.glsl: -------------------------------------------------------------------------------- 1 | 2 | // @[immutable_samplers([ 3 | // ( 4 | // mag_filter: Linear, 5 | // min_filter: Linear, 6 | // mip_map_mode: Linear, 7 | // address_mode_u: Repeat, 8 | // address_mode_v: Repeat, 9 | // address_mode_w: Repeat, 10 | // ) 11 | // ])] 12 | layout (set = 0, binding = 0) uniform sampler smp; 13 | 14 | layout (set = 0, binding = 1) uniform texture2D tex; 15 | -------------------------------------------------------------------------------- /skulpin-renderer/shaders/glsl/skia.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | #extension GL_ARB_separate_shader_objects : enable 3 | 4 | #include "skia.glsl" 5 | 6 | // @[semantic("POSITION")] 7 | layout(location = 0) in vec2 inPosition; 8 | // @[semantic("TEXCOORD")] 9 | layout(location = 1) in vec2 inTexCoord; 10 | 11 | layout(location = 0) out vec2 fragTexCoord; 12 | 13 | void main() { 14 | gl_Position = vec4(inPosition, 0.0, 1.0); 15 | fragTexCoord = inTexCoord; 16 | } -------------------------------------------------------------------------------- /skulpin-renderer/shaders/out/skia.frag.cookedshaderpackage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aclysma/skulpin/6a7fa663ead00e875e3e290be5a67b2c74e10a31/skulpin-renderer/shaders/out/skia.frag.cookedshaderpackage -------------------------------------------------------------------------------- /skulpin-renderer/shaders/out/skia.vert.cookedshaderpackage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aclysma/skulpin/6a7fa663ead00e875e3e290be5a67b2c74e10a31/skulpin-renderer/shaders/out/skia.vert.cookedshaderpackage -------------------------------------------------------------------------------- /skulpin-renderer/src/coordinates.rs: -------------------------------------------------------------------------------- 1 | //! Defines physical and logical coordinates. This is heavily based on winit's design. 2 | 3 | use rafx::api::RafxExtents2D; 4 | 5 | /// A size in raw pixels 6 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 7 | pub struct PhysicalSize { 8 | pub width: u32, 9 | pub height: u32, 10 | } 11 | 12 | impl PhysicalSize { 13 | pub fn new( 14 | width: u32, 15 | height: u32, 16 | ) -> Self { 17 | PhysicalSize { width, height } 18 | } 19 | 20 | pub fn to_logical( 21 | self, 22 | scale_factor: f64, 23 | ) -> LogicalSize { 24 | LogicalSize { 25 | width: (self.width as f64 / scale_factor).round() as u32, 26 | height: (self.height as f64 / scale_factor).round() as u32, 27 | } 28 | } 29 | } 30 | 31 | /// A size in raw pixels * a scaling factor. The scaling factor could be increased for hidpi 32 | /// displays 33 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 34 | pub struct LogicalSize { 35 | pub width: u32, 36 | pub height: u32, 37 | } 38 | 39 | impl LogicalSize { 40 | pub fn new( 41 | width: u32, 42 | height: u32, 43 | ) -> Self { 44 | LogicalSize { width, height } 45 | } 46 | 47 | pub fn to_physical( 48 | self, 49 | scale_factor: f64, 50 | ) -> PhysicalSize { 51 | PhysicalSize { 52 | width: (self.width as f64 * scale_factor).round() as u32, 53 | height: (self.height as f64 * scale_factor).round() as u32, 54 | } 55 | } 56 | } 57 | 58 | /// A size that's either physical or logical. 59 | #[derive(Debug, Copy, Clone, PartialEq)] 60 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 61 | pub enum Size { 62 | Physical(PhysicalSize), 63 | Logical(LogicalSize), 64 | } 65 | 66 | impl From for Size { 67 | fn from(physical_size: PhysicalSize) -> Self { 68 | Size::Physical(physical_size) 69 | } 70 | } 71 | 72 | impl From for Size { 73 | fn from(logical_size: LogicalSize) -> Self { 74 | Size::Logical(logical_size) 75 | } 76 | } 77 | 78 | impl Size { 79 | pub fn new>(size: S) -> Size { 80 | size.into() 81 | } 82 | 83 | pub fn to_logical( 84 | &self, 85 | scale_factor: f64, 86 | ) -> LogicalSize { 87 | match *self { 88 | Size::Physical(size) => size.to_logical(scale_factor), 89 | Size::Logical(size) => size, 90 | } 91 | } 92 | 93 | pub fn to_physical( 94 | &self, 95 | scale_factor: f64, 96 | ) -> PhysicalSize { 97 | match *self { 98 | Size::Physical(size) => size, 99 | Size::Logical(size) => size.to_physical(scale_factor), 100 | } 101 | } 102 | } 103 | 104 | /// Default coordinate system to use 105 | #[derive(Copy, Clone)] 106 | pub enum CoordinateSystem { 107 | /// Logical coordinates will use (0,0) top-left and (+X,+Y) right-bottom where X/Y is the logical 108 | /// size of the window. Logical size applies a multiplier for hi-dpi displays. For example, many 109 | /// 4K displays would probably have a high-dpi factor of 2.0, simulating a 1080p display. 110 | Logical, 111 | 112 | /// Physical coordinates will use (0,0) top-left and (+X,+Y) right-bottom where X/Y is the raw 113 | /// number of pixels. 114 | Physical, 115 | 116 | /// Visible range allows specifying an arbitrary coordinate system. For example, if you want X to 117 | /// range (100, 300) and Y to range (-100, 400), you can do that. It's likely you'd want to 118 | /// determine either X or Y using the aspect ratio to avoid stretching. 119 | VisibleRange(skia_safe::Rect, skia_safe::matrix::ScaleToFit), 120 | 121 | /// FixedWidth will use the given center position and width, and calculate appropriate Y extents 122 | /// for the current aspect ratio 123 | FixedWidth(skia_safe::Point, f32), 124 | 125 | /// Do not modify the canvas matrix 126 | None, 127 | } 128 | 129 | impl Default for CoordinateSystem { 130 | fn default() -> Self { 131 | CoordinateSystem::Logical 132 | } 133 | } 134 | 135 | /// Provides a convenient method to set the canvas coordinate system to commonly-desired defaults. 136 | /// 137 | /// * Physical coordinates will use (0,0) top-left and (+X,+Y) right-bottom where X/Y is the raw 138 | /// number of pixels. 139 | /// * Logical coordinates will use (0,0) top-left and (+X,+Y) right-bottom where X/Y is the logical 140 | /// size of the window. Logical size applies a multiplier for hi-dpi displays. For example, many 141 | /// 4K displays would probably have a high-dpi factor of 2.0, simulating a 1080p display. 142 | /// * Visible range allows specifying an arbitrary coordinate system. For example, if you want X to 143 | /// range (100, 300) and Y to range (-100, 400), you can do that. It's likely you'd want to 144 | /// determine either X or Y using the aspect ratio to avoid stretching. 145 | /// * FixedWidth will use the given center position and width, and calculate appropriate Y extents 146 | /// for the current aspect ratio 147 | /// * See `use_physical_coordinates`, `use_logical_coordinates`, or `use_visible_range` to choose 148 | /// between these options. 149 | /// 150 | /// For custom behavior, it's always possible to call `canvas.reset_matrix()` and set up the matrix 151 | /// manually 152 | #[derive(Clone)] 153 | pub struct CoordinateSystemHelper { 154 | surface_extents: RafxExtents2D, 155 | window_logical_size: LogicalSize, 156 | window_physical_size: PhysicalSize, 157 | scale_factor: f64, 158 | } 159 | 160 | impl CoordinateSystemHelper { 161 | /// Create a CoordinateSystemHelper for a window of the given parameters 162 | pub fn new( 163 | surface_extents: RafxExtents2D, 164 | scale_factor: f64, 165 | ) -> Self { 166 | let window_physical_size = PhysicalSize { 167 | width: surface_extents.width, 168 | height: surface_extents.height, 169 | }; 170 | 171 | let window_logical_size = window_physical_size.to_logical(scale_factor); 172 | 173 | CoordinateSystemHelper { 174 | surface_extents, 175 | window_logical_size, 176 | window_physical_size, 177 | scale_factor, 178 | } 179 | } 180 | 181 | /// Get the raw pixel size of the surface to which we are drawing 182 | pub fn surface_extents(&self) -> RafxExtents2D { 183 | self.surface_extents 184 | } 185 | 186 | /// Get the logical inner size of the window 187 | pub fn window_logical_size(&self) -> LogicalSize { 188 | self.window_logical_size 189 | } 190 | 191 | /// Get the physical inner size of the window 192 | pub fn window_physical_size(&self) -> PhysicalSize { 193 | self.window_physical_size 194 | } 195 | 196 | /// Get the multiplier used for high-dpi displays. For example, a 4K display simulating a 1080p 197 | /// display will use a factor of 2.0 198 | pub fn scale_factor(&self) -> f64 { 199 | self.scale_factor 200 | } 201 | 202 | /// Use raw pixels for the coordinate system. Top-left is (0, 0), bottom-right is (+X, +Y) 203 | pub fn use_physical_coordinates( 204 | &self, 205 | canvas: &mut skia_safe::Canvas, 206 | ) { 207 | // For raw physical pixels, no need to do anything 208 | canvas.reset_matrix(); 209 | } 210 | 211 | /// Use logical coordinates for the coordinate system. Top-left is (0, 0), bottom-right is 212 | /// (+X, +Y). Logical size applies a multiplier for hi-dpi displays. For example, many 213 | /// 4K displays would probably have a high-dpi factor of 2.0, simulating a 1080p display. 214 | pub fn use_logical_coordinates( 215 | &self, 216 | canvas: &mut skia_safe::Canvas, 217 | ) { 218 | // To handle hi-dpi displays, we need to compare the logical size of the window with the 219 | // actual canvas size. Critically, the canvas size won't necessarily be the size of the 220 | // window in physical pixels. 221 | let scale = ( 222 | (f64::from(self.surface_extents.width) / self.window_logical_size.width as f64) as f32, 223 | (f64::from(self.surface_extents.height) / self.window_logical_size.height as f64) 224 | as f32, 225 | ); 226 | 227 | canvas.reset_matrix(); 228 | canvas.scale(scale); 229 | } 230 | 231 | /// Maps the given visible range to the render surface. For example, if you want a coordinate 232 | /// system where (0, 0) is the center of the screen, the X bounds are (-640, 640) and Y bounds 233 | /// are (-360, 360) you can specify that here. 234 | /// 235 | /// The scale_to_fit parameter will choose how to handle an inconsistent aspect ratio between 236 | /// visible_range and the surface. Common choices would be `skia_safe::matrix::ScaleToFit::Fill` 237 | /// to allow stretching or `skia_safe::matrix::ScaleToFit::Center` to scale such that the full 238 | /// visible_range is included (even if it means there is extra showing) 239 | /// 240 | /// Skia assumes that left is less than right and that top is less than bottom. If you provide 241 | /// a visible range that violates this, this function will apply a scaling factor to try to 242 | /// provide intuitive behavior. However, this can have side effects like upside-down text. 243 | /// 244 | /// See https://skia.org/user/api/SkMatrix_Reference#SkMatrix_setRectToRect 245 | /// See https://skia.org/user/api/SkMatrix_Reference#SkMatrix_ScaleToFit 246 | pub fn use_visible_range( 247 | &self, 248 | canvas: &mut skia_safe::Canvas, 249 | mut visible_range: skia_safe::Rect, 250 | scale_to_fit: skia_safe::matrix::ScaleToFit, 251 | ) -> Result<(), ()> { 252 | let x_scale = if visible_range.left <= visible_range.right { 253 | 1.0 254 | } else { 255 | visible_range.left *= -1.0; 256 | visible_range.right *= -1.0; 257 | -1.0 258 | }; 259 | 260 | let y_scale = if visible_range.top <= visible_range.bottom { 261 | 1.0 262 | } else { 263 | visible_range.top *= -1.0; 264 | visible_range.bottom *= -1.0; 265 | -1.0 266 | }; 267 | 268 | let dst = skia_safe::Rect { 269 | left: 0.0, 270 | top: 0.0, 271 | right: self.surface_extents.width as f32, 272 | bottom: self.surface_extents.height as f32, 273 | }; 274 | 275 | let m = skia_safe::Matrix::from_rect_to_rect(visible_range, dst, scale_to_fit); 276 | match m { 277 | Some(m) => { 278 | canvas.set_matrix(&m.into()); 279 | canvas.scale((x_scale, y_scale)); 280 | Ok(()) 281 | } 282 | None => Err(()), 283 | } 284 | } 285 | 286 | /// Given a center position and half-extents for X, calculate an appropriate Y half-extents that 287 | /// is consistent with the aspect ratio. 288 | pub fn use_fixed_width( 289 | &self, 290 | canvas: &mut skia_safe::Canvas, 291 | center: skia_safe::Point, 292 | x_half_extents: f32, 293 | ) -> Result<(), ()> { 294 | let left = center.x - x_half_extents; 295 | let right = center.x + x_half_extents; 296 | let y_half_extents = x_half_extents as f32 297 | / (self.surface_extents.width as f32 / self.surface_extents.height as f32); 298 | let top = center.y - y_half_extents; 299 | let bottom = center.y + y_half_extents; 300 | 301 | let rect = skia_safe::Rect { 302 | left, 303 | top, 304 | right, 305 | bottom, 306 | }; 307 | 308 | self.use_visible_range(canvas, rect, skia_safe::matrix::ScaleToFit::Fill) 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /skulpin-renderer/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | pub use rafx; 5 | pub use skia_safe; 6 | pub use skia_bindings; 7 | 8 | pub const MAX_FRAMES_IN_FLIGHT: usize = 2; 9 | 10 | mod skia_support; 11 | pub use skia_support::VkSkiaContext; 12 | pub use skia_support::VkSkiaSurface; 13 | 14 | mod renderer; 15 | pub use renderer::RendererBuilder; 16 | pub use renderer::Renderer; 17 | pub use renderer::ValidationMode; 18 | 19 | mod coordinates; 20 | pub use coordinates::Size; 21 | pub use coordinates::LogicalSize; 22 | pub use coordinates::PhysicalSize; 23 | pub use coordinates::CoordinateSystem; 24 | pub use coordinates::CoordinateSystemHelper; 25 | -------------------------------------------------------------------------------- /skulpin-renderer/src/renderer.rs: -------------------------------------------------------------------------------- 1 | use rafx::api::*; 2 | use rafx::render_features::*; 3 | use rafx::framework::*; 4 | 5 | use super::CoordinateSystemHelper; 6 | use super::CoordinateSystem; 7 | use rafx::api::raw_window_handle::HasRawWindowHandle; 8 | use std::sync::Arc; 9 | use crate::VkSkiaContext; 10 | use crate::skia_support::VkSkiaSurface; 11 | 12 | use rafx::api::RafxValidationMode; 13 | 14 | /// Controls if validation is enabled or not. Validation layers require the vulkan SDK to be 15 | /// installed. 16 | #[derive(Copy, Clone, Debug)] 17 | pub enum ValidationMode { 18 | /// Do not enable validation. 19 | Disabled, 20 | 21 | /// Enable validation if possible (i.e. vulkan SDK is installed) 22 | EnabledIfAvailable, 23 | 24 | /// Enable validation, and fail if we cannot enable it. This requires the vulkan SDK to be 25 | /// installed. 26 | Enabled, 27 | } 28 | 29 | impl Default for ValidationMode { 30 | fn default() -> Self { 31 | ValidationMode::Disabled 32 | } 33 | } 34 | 35 | impl Into for ValidationMode { 36 | fn into(self) -> RafxValidationMode { 37 | match self { 38 | ValidationMode::Disabled => RafxValidationMode::Disabled, 39 | ValidationMode::Enabled => RafxValidationMode::Enabled, 40 | ValidationMode::EnabledIfAvailable => RafxValidationMode::EnabledIfAvailable, 41 | } 42 | } 43 | } 44 | 45 | /// A builder to create the renderer. It's easier to use AppBuilder and implement an AppHandler, but 46 | /// initializing the renderer and maintaining the window yourself allows for more customization 47 | #[derive(Default)] 48 | pub struct RendererBuilder { 49 | coordinate_system: CoordinateSystem, 50 | vsync_enabled: bool, 51 | validation_mode: ValidationMode, 52 | } 53 | 54 | impl RendererBuilder { 55 | /// Construct the renderer builder with default options 56 | pub fn new() -> Self { 57 | RendererBuilder { 58 | coordinate_system: Default::default(), 59 | vsync_enabled: true, 60 | validation_mode: ValidationMode::default(), 61 | } 62 | } 63 | 64 | /// Determine the coordinate system to use for the canvas. This can be overridden by using the 65 | /// canvas sizer passed into the draw callback 66 | pub fn coordinate_system( 67 | mut self, 68 | coordinate_system: CoordinateSystem, 69 | ) -> Self { 70 | self.coordinate_system = coordinate_system; 71 | self 72 | } 73 | 74 | pub fn vsync_enabled( 75 | mut self, 76 | vsync_enabled: bool, 77 | ) -> Self { 78 | self.vsync_enabled = vsync_enabled; 79 | self 80 | } 81 | 82 | pub fn validation_mode( 83 | mut self, 84 | validation_mode: ValidationMode, 85 | ) -> Self { 86 | self.validation_mode = validation_mode; 87 | self 88 | } 89 | 90 | /// Builds the renderer. The window that's passed in will be used for creating the swapchain 91 | pub fn build( 92 | self, 93 | window: &dyn HasRawWindowHandle, 94 | window_size: RafxExtents2D, 95 | ) -> RafxResult { 96 | Renderer::new( 97 | window, 98 | window_size, 99 | self.coordinate_system, 100 | self.vsync_enabled, 101 | self.validation_mode, 102 | ) 103 | } 104 | } 105 | struct SwapchainEventListener<'a> { 106 | skia_context: &'a mut VkSkiaContext, 107 | skia_surface: &'a mut Option, 108 | resource_manager: &'a ResourceManager, 109 | } 110 | 111 | impl<'a> RafxSwapchainEventListener for SwapchainEventListener<'a> { 112 | fn swapchain_created( 113 | &mut self, 114 | _device_context: &RafxDeviceContext, 115 | swapchain: &RafxSwapchain, 116 | ) -> RafxResult<()> { 117 | *self.skia_surface = Some(VkSkiaSurface::new( 118 | &self.resource_manager, 119 | &mut self.skia_context, 120 | RafxExtents2D { 121 | width: swapchain.swapchain_def().width.max(1), 122 | height: swapchain.swapchain_def().height.max(1), 123 | }, 124 | )?); 125 | 126 | Ok(()) 127 | } 128 | 129 | fn swapchain_destroyed( 130 | &mut self, 131 | _device_context: &RafxDeviceContext, 132 | _swapchain: &RafxSwapchain, 133 | ) -> RafxResult<()> { 134 | *self.skia_surface = None; 135 | 136 | Ok(()) 137 | } 138 | } 139 | 140 | /// Vulkan renderer that creates and manages the vulkan instance, device, swapchain, and 141 | /// render passes. 142 | pub struct Renderer { 143 | // Ordered in drop order 144 | pub coordinate_system: CoordinateSystem, 145 | pub skia_surface: Option, 146 | pub skia_context: VkSkiaContext, 147 | pub skia_material_pass: MaterialPass, 148 | pub graphics_queue: RafxQueue, 149 | pub swapchain_helper: RafxSwapchainHelper, 150 | pub resource_manager: ResourceManager, 151 | #[allow(dead_code)] 152 | pub api: RafxApi, 153 | } 154 | 155 | lazy_static::lazy_static! { 156 | pub static ref RENDER_REGISTRY: RenderRegistry = RenderRegistryBuilder::default() 157 | .register_render_phase::("opaque") 158 | .build(); 159 | } 160 | 161 | impl Renderer { 162 | /// Create the renderer 163 | pub fn new( 164 | window: &dyn HasRawWindowHandle, 165 | window_size: RafxExtents2D, 166 | coordinate_system: CoordinateSystem, 167 | vsync_enabled: bool, 168 | validation_mode: ValidationMode, 169 | ) -> RafxResult { 170 | let api_def = RafxApiDefVulkan { 171 | validation_mode: validation_mode.into(), 172 | ..Default::default() 173 | }; 174 | 175 | let api = unsafe { RafxApi::new_vulkan(window, &Default::default(), &api_def) }?; 176 | let device_context = api.device_context(); 177 | 178 | let resource_manager = 179 | rafx::framework::ResourceManager::new(&device_context, &RENDER_REGISTRY); 180 | 181 | let swapchain = device_context.create_swapchain( 182 | window, 183 | &RafxSwapchainDef { 184 | width: window_size.width, 185 | height: window_size.height, 186 | enable_vsync: vsync_enabled, 187 | }, 188 | )?; 189 | 190 | let graphics_queue = device_context.create_queue(RafxQueueType::Graphics)?; 191 | 192 | let mut skia_context = VkSkiaContext::new(&device_context, &graphics_queue); 193 | let mut skia_surface = None; 194 | 195 | let swapchain_helper = RafxSwapchainHelper::new( 196 | &device_context, 197 | swapchain, 198 | Some(&mut SwapchainEventListener { 199 | skia_context: &mut skia_context, 200 | skia_surface: &mut skia_surface, 201 | resource_manager: &resource_manager, 202 | }), 203 | )?; 204 | 205 | let resource_context = resource_manager.resource_context(); 206 | 207 | let skia_material_pass = Self::load_material_pass( 208 | &resource_context, 209 | include_bytes!("../shaders/out/skia.vert.cookedshaderpackage"), 210 | include_bytes!("../shaders/out/skia.frag.cookedshaderpackage"), 211 | FixedFunctionState { 212 | rasterizer_state: Default::default(), 213 | depth_state: Default::default(), 214 | blend_state: Default::default(), 215 | }, 216 | )?; 217 | 218 | Ok(Renderer { 219 | api, 220 | resource_manager, 221 | swapchain_helper, 222 | graphics_queue, 223 | skia_material_pass, 224 | coordinate_system, 225 | skia_context, 226 | skia_surface, 227 | }) 228 | } 229 | 230 | /// Call to render a frame. This can block for certain presentation modes. This will rebuild 231 | /// the swapchain if necessary. 232 | pub fn draw( 233 | &mut self, 234 | window_size: RafxExtents2D, 235 | scale_factor: f64, 236 | f: F, 237 | ) -> RafxResult<()> { 238 | // 239 | // Begin the frame 240 | // 241 | let frame = self.swapchain_helper.acquire_next_image( 242 | window_size.width, 243 | window_size.height, 244 | Some(&mut SwapchainEventListener { 245 | skia_context: &mut self.skia_context, 246 | skia_surface: &mut self.skia_surface, 247 | resource_manager: &self.resource_manager, 248 | }), 249 | )?; 250 | 251 | // Acquiring an image means a prior frame completely finished processing 252 | self.resource_manager.on_frame_complete()?; 253 | 254 | // 255 | // Do skia drawing (including the user's callback) 256 | // 257 | let mut canvas = self.skia_surface.as_mut().unwrap().surface.canvas(); 258 | 259 | let coordinate_system_helper = CoordinateSystemHelper::new(window_size, scale_factor); 260 | 261 | match self.coordinate_system { 262 | CoordinateSystem::None => {} 263 | CoordinateSystem::Physical => { 264 | coordinate_system_helper.use_physical_coordinates(&mut canvas) 265 | } 266 | CoordinateSystem::Logical => { 267 | coordinate_system_helper.use_logical_coordinates(&mut canvas) 268 | } 269 | CoordinateSystem::VisibleRange(range, scale_to_fit) => coordinate_system_helper 270 | .use_visible_range(&mut canvas, range, scale_to_fit) 271 | .unwrap(), 272 | CoordinateSystem::FixedWidth(center, x_half_extents) => coordinate_system_helper 273 | .use_fixed_width(&mut canvas, center, x_half_extents) 274 | .unwrap(), 275 | } 276 | 277 | f(&mut canvas, coordinate_system_helper); 278 | self.skia_context.context.flush_and_submit(); 279 | 280 | // 281 | // Convert the skia texture to a shader resources, draw a quad, and convert it back to a 282 | // render target 283 | // 284 | let mut descriptor_set_allocator = self.resource_manager.create_descriptor_set_allocator(); 285 | let mut descriptor_set = descriptor_set_allocator.create_dyn_descriptor_set_uninitialized( 286 | &self 287 | .skia_material_pass 288 | .material_pass_resource 289 | .get_raw() 290 | .descriptor_set_layouts[0], 291 | )?; 292 | 293 | descriptor_set.set_image(1, &self.skia_surface.as_ref().unwrap().image_view); 294 | 295 | descriptor_set.flush(&mut descriptor_set_allocator)?; 296 | descriptor_set_allocator.flush_changes()?; 297 | 298 | let mut command_pool = self 299 | .resource_manager 300 | .dyn_command_pool_allocator() 301 | .allocate_dyn_pool( 302 | &self.graphics_queue, 303 | &RafxCommandPoolDef { transient: false }, 304 | 0, 305 | )?; 306 | 307 | let command_buffer = command_pool.allocate_dyn_command_buffer(&RafxCommandBufferDef { 308 | is_secondary: false, 309 | })?; 310 | 311 | command_buffer.begin()?; 312 | 313 | command_buffer.cmd_resource_barrier( 314 | &[], 315 | &[ 316 | RafxTextureBarrier { 317 | texture: frame.swapchain_texture(), 318 | array_slice: None, 319 | mip_slice: None, 320 | src_state: RafxResourceState::PRESENT, 321 | dst_state: RafxResourceState::RENDER_TARGET, 322 | queue_transition: RafxBarrierQueueTransition::None, 323 | }, 324 | RafxTextureBarrier { 325 | texture: &self 326 | .skia_surface 327 | .as_ref() 328 | .unwrap() 329 | .image_view 330 | .get_raw() 331 | .image 332 | .get_raw() 333 | .image, 334 | array_slice: None, 335 | mip_slice: None, 336 | src_state: RafxResourceState::RENDER_TARGET, 337 | dst_state: RafxResourceState::SHADER_RESOURCE, 338 | queue_transition: RafxBarrierQueueTransition::None, 339 | }, 340 | ], 341 | )?; 342 | 343 | command_buffer.cmd_begin_render_pass( 344 | &[RafxColorRenderTargetBinding { 345 | texture: frame.swapchain_texture(), 346 | load_op: RafxLoadOp::DontCare, 347 | store_op: RafxStoreOp::Store, 348 | clear_value: RafxColorClearValue([0.0, 0.0, 0.0, 0.0]), 349 | mip_slice: Default::default(), 350 | array_slice: Default::default(), 351 | resolve_target: Default::default(), 352 | resolve_store_op: Default::default(), 353 | resolve_mip_slice: Default::default(), 354 | resolve_array_slice: Default::default(), 355 | }], 356 | None, 357 | )?; 358 | 359 | let pipeline = self 360 | .resource_manager 361 | .graphics_pipeline_cache() 362 | .get_or_create_graphics_pipeline( 363 | OpaqueRenderPhase::render_phase_index(), 364 | &self.skia_material_pass.material_pass_resource, 365 | &GraphicsPipelineRenderTargetMeta::new( 366 | vec![self.swapchain_helper.format()], 367 | None, 368 | RafxSampleCount::SampleCount1, 369 | ), 370 | &*VERTEX_LAYOUT, 371 | )?; 372 | 373 | let vertex_buffer = self 374 | .resource_manager 375 | .device_context() 376 | .create_buffer(&RafxBufferDef::for_staging_vertex_buffer_data(&VERTEX_LIST))?; 377 | vertex_buffer.copy_to_host_visible_buffer(&VERTEX_LIST)?; 378 | 379 | let vertex_buffer = self 380 | .resource_manager 381 | .create_dyn_resource_allocator_set() 382 | .insert_buffer(vertex_buffer); 383 | 384 | command_buffer.cmd_bind_pipeline(&*pipeline.get_raw().pipeline)?; 385 | command_buffer.cmd_bind_vertex_buffers( 386 | 0, 387 | &[RafxVertexBufferBinding { 388 | buffer: &*vertex_buffer.get_raw().buffer, 389 | byte_offset: 0, 390 | }], 391 | )?; 392 | descriptor_set.bind(&command_buffer)?; 393 | 394 | command_buffer.cmd_draw(6, 0)?; 395 | 396 | command_buffer.cmd_end_render_pass()?; 397 | 398 | command_buffer.cmd_resource_barrier( 399 | &[], 400 | &[ 401 | RafxTextureBarrier { 402 | texture: frame.swapchain_texture(), 403 | array_slice: None, 404 | mip_slice: None, 405 | src_state: RafxResourceState::RENDER_TARGET, 406 | dst_state: RafxResourceState::PRESENT, 407 | queue_transition: RafxBarrierQueueTransition::None, 408 | }, 409 | RafxTextureBarrier { 410 | texture: &self 411 | .skia_surface 412 | .as_ref() 413 | .unwrap() 414 | .image_view 415 | .get_raw() 416 | .image 417 | .get_raw() 418 | .image, 419 | array_slice: None, 420 | mip_slice: None, 421 | src_state: RafxResourceState::SHADER_RESOURCE, 422 | dst_state: RafxResourceState::RENDER_TARGET, 423 | queue_transition: RafxBarrierQueueTransition::None, 424 | }, 425 | ], 426 | )?; 427 | 428 | command_buffer.end()?; 429 | 430 | frame.present(&self.graphics_queue, &[&command_buffer])?; 431 | 432 | Ok(()) 433 | } 434 | 435 | fn load_material_pass( 436 | resource_context: &ResourceContext, 437 | cooked_vertex_shader_bytes: &[u8], 438 | cooked_fragment_shader_bytes: &[u8], 439 | fixed_function_state: FixedFunctionState, 440 | ) -> RafxResult { 441 | let cooked_vertex_shader_stage = 442 | bincode::deserialize::(cooked_vertex_shader_bytes) 443 | .map_err(|x| format!("Failed to deserialize cooked shader: {:?}", x))?; 444 | let vertex_shader_module = resource_context 445 | .resources() 446 | .get_or_create_shader_module_from_cooked_package(&cooked_vertex_shader_stage)?; 447 | let vertex_entry_point = cooked_vertex_shader_stage 448 | .find_entry_point("main") 449 | .unwrap() 450 | .clone(); 451 | 452 | // Create the fragment shader module and find the entry point 453 | let cooked_fragment_shader_stage = 454 | bincode::deserialize::(cooked_fragment_shader_bytes) 455 | .map_err(|x| format!("Failed to deserialize cooked shader: {:?}", x))?; 456 | let fragment_shader_module = resource_context 457 | .resources() 458 | .get_or_create_shader_module_from_cooked_package(&cooked_fragment_shader_stage)?; 459 | let fragment_entry_point = cooked_fragment_shader_stage 460 | .find_entry_point("main") 461 | .unwrap() 462 | .clone(); 463 | 464 | let fixed_function_state = Arc::new(fixed_function_state); 465 | 466 | let material_pass = MaterialPass::new( 467 | &resource_context, 468 | fixed_function_state, 469 | vec![vertex_shader_module, fragment_shader_module], 470 | &[&vertex_entry_point, &fragment_entry_point], 471 | )?; 472 | 473 | Ok(material_pass) 474 | } 475 | } 476 | 477 | impl Drop for Renderer { 478 | fn drop(&mut self) { 479 | debug!("destroying Renderer"); 480 | self.graphics_queue.wait_for_queue_idle().unwrap(); 481 | debug!("destroyed Renderer"); 482 | } 483 | } 484 | 485 | rafx::declare_render_phase!( 486 | OpaqueRenderPhase, 487 | OPAQUE_RENDER_PHASE_INDEX, 488 | opaque_render_phase_sort_submit_nodes 489 | ); 490 | 491 | fn opaque_render_phase_sort_submit_nodes(_submit_nodes: &mut Vec) { 492 | // No sort needed 493 | } 494 | 495 | #[derive(Clone, Debug, Copy)] 496 | struct Vertex { 497 | pos: [f32; 2], 498 | tex_coord: [f32; 2], 499 | } 500 | 501 | const VERTEX_LIST: [Vertex; 6] = [ 502 | Vertex { 503 | pos: [-1.0, -1.0], 504 | tex_coord: [0.0, 1.0], 505 | }, 506 | Vertex { 507 | pos: [1.0, -1.0], 508 | tex_coord: [1.0, 1.0], 509 | }, 510 | Vertex { 511 | pos: [1.0, 1.0], 512 | tex_coord: [1.0, 0.0], 513 | }, 514 | Vertex { 515 | pos: [1.0, 1.0], 516 | tex_coord: [1.0, 0.0], 517 | }, 518 | Vertex { 519 | pos: [-1.0, 1.0], 520 | tex_coord: [0.0, 0.0], 521 | }, 522 | Vertex { 523 | pos: [-1.0, -1.0], 524 | tex_coord: [0.0, 1.0], 525 | }, 526 | ]; 527 | 528 | lazy_static::lazy_static! { 529 | pub static ref VERTEX_LAYOUT : VertexDataSetLayout = { 530 | use rafx::api::RafxFormat; 531 | 532 | let vertex = Vertex { 533 | pos: Default::default(), 534 | tex_coord: Default::default(), 535 | }; 536 | 537 | VertexDataLayout::build_vertex_layout(&vertex, RafxVertexAttributeRate::Vertex, |builder, vertex| { 538 | builder.add_member(&vertex.pos, "POSITION", RafxFormat::R32G32_SFLOAT); 539 | builder.add_member(&vertex.tex_coord, "TEXCOORD", RafxFormat::R32G32_SFLOAT); 540 | }).into_set(RafxPrimitiveTopology::TriangleList) 541 | }; 542 | } 543 | -------------------------------------------------------------------------------- /skulpin-renderer/src/skia_support.rs: -------------------------------------------------------------------------------- 1 | use rafx::api::*; 2 | use rafx::framework::*; 3 | use rafx::api::ash; 4 | use ash::vk; 5 | use ash::version::InstanceV1_0; 6 | use rafx::api::vulkan::RafxRawImageVulkan; 7 | 8 | /// Handles setting up skia to use the same vulkan instance we initialize 9 | pub struct VkSkiaContext { 10 | pub context: skia_safe::gpu::DirectContext, 11 | } 12 | 13 | impl VkSkiaContext { 14 | pub fn new( 15 | device_context: &RafxDeviceContext, 16 | queue: &RafxQueue, 17 | ) -> Self { 18 | use vk::Handle; 19 | 20 | let vk_device_context = device_context.vk_device_context().unwrap(); 21 | let entry = vk_device_context.entry(); 22 | let instance = vk_device_context.instance(); 23 | let physical_device = vk_device_context.physical_device(); 24 | let device = vk_device_context.device(); 25 | 26 | let graphics_queue_family = vk_device_context 27 | .queue_family_indices() 28 | .graphics_queue_family_index; 29 | 30 | let get_proc = |of| unsafe { 31 | match Self::get_proc(instance, entry, of) { 32 | Some(f) => f as _, 33 | None => { 34 | error!("resolve of {} failed", of.name().to_str().unwrap()); 35 | std::ptr::null() 36 | } 37 | } 38 | }; 39 | 40 | info!( 41 | "Setting up skia backend context with queue family index {}", 42 | graphics_queue_family 43 | ); 44 | 45 | let backend_context = unsafe { 46 | let vk_queue_handle = *queue.vk_queue().unwrap().queue().queue().lock().unwrap(); 47 | skia_safe::gpu::vk::BackendContext::new( 48 | instance.handle().as_raw() as _, 49 | physical_device.as_raw() as _, 50 | device.handle().as_raw() as _, 51 | ( 52 | vk_queue_handle.as_raw() as _, 53 | graphics_queue_family as usize, 54 | ), 55 | &get_proc, 56 | ) 57 | }; 58 | 59 | let context = skia_safe::gpu::DirectContext::new_vulkan(&backend_context, None).unwrap(); 60 | 61 | VkSkiaContext { context } 62 | } 63 | 64 | unsafe fn get_proc( 65 | instance: &ash::Instance, 66 | entry: &E, 67 | of: skia_safe::gpu::vk::GetProcOf, 68 | ) -> vk::PFN_vkVoidFunction { 69 | use rafx::api::ash::vk::Handle; 70 | match of { 71 | skia_safe::gpu::vk::GetProcOf::Instance(instance_proc, name) => { 72 | // Instead of forcing skia to use vk 1.0.0, 73 | // use the instance version that is the most appropriate. 74 | let ash_instance = vk::Instance::from_raw(instance_proc as _); 75 | entry.get_instance_proc_addr(ash_instance, name) 76 | } 77 | skia_safe::gpu::vk::GetProcOf::Device(device_proc, name) => { 78 | let ash_device = vk::Device::from_raw(device_proc as _); 79 | instance.get_device_proc_addr(ash_device, name) 80 | } 81 | } 82 | } 83 | } 84 | 85 | /// Wraps a skia surface/canvas that can be drawn on and makes the vulkan resources accessible 86 | pub struct VkSkiaSurface { 87 | pub device_context: RafxDeviceContext, 88 | pub image_view: ResourceArc, 89 | pub surface: skia_safe::Surface, 90 | pub texture: skia_safe::gpu::BackendTexture, 91 | } 92 | 93 | impl VkSkiaSurface { 94 | pub fn get_image_from_skia_texture(texture: &skia_safe::gpu::BackendTexture) -> vk::Image { 95 | unsafe { std::mem::transmute(texture.vulkan_image_info().unwrap().image.as_ref().unwrap()) } 96 | } 97 | 98 | pub fn new( 99 | resource_manager: &ResourceManager, 100 | context: &mut VkSkiaContext, 101 | extents: RafxExtents2D, 102 | ) -> RafxResult { 103 | assert!(extents.width > 0); 104 | assert!(extents.height > 0); 105 | // The "native" color type is based on platform. For example, on Windows it's BGR and on 106 | // MacOS it's RGB 107 | let color_type = skia_safe::ColorType::N32; 108 | let alpha_type = skia_safe::AlphaType::Premul; 109 | let color_space = Some(skia_safe::ColorSpace::new_srgb_linear()); 110 | 111 | let image_info = skia_safe::ImageInfo::new( 112 | (extents.width as i32, extents.height as i32), 113 | color_type, 114 | alpha_type, 115 | color_space, 116 | ); 117 | 118 | let mut surface = skia_safe::Surface::new_render_target( 119 | &mut context.context, 120 | skia_safe::Budgeted::Yes, 121 | &image_info, 122 | None, 123 | skia_safe::gpu::SurfaceOrigin::TopLeft, 124 | None, 125 | false, 126 | ) 127 | .unwrap(); 128 | 129 | let texture = surface 130 | .get_backend_texture(skia_safe::surface::BackendHandleAccess::FlushRead) 131 | .unwrap(); 132 | let image = Self::get_image_from_skia_texture(&texture); 133 | 134 | // According to docs, kN32_SkColorType can only be kRGBA_8888_SkColorType or 135 | // kBGRA_8888_SkColorType. Whatever it is, we need to set up the image view with the 136 | // matching format 137 | let format = match color_type { 138 | skia_safe::ColorType::RGBA8888 => RafxFormat::R8G8B8A8_UNORM, 139 | skia_safe::ColorType::BGRA8888 => RafxFormat::B8G8R8A8_UNORM, 140 | _ => { 141 | warn!("Unexpected native color type {:?}", color_type); 142 | RafxFormat::R8G8B8A8_UNORM 143 | } 144 | }; 145 | 146 | let device_context = resource_manager.device_context(); 147 | 148 | let raw_image = RafxRawImageVulkan { 149 | allocation: None, 150 | image, 151 | }; 152 | 153 | let image = rafx::api::vulkan::RafxTextureVulkan::from_existing( 154 | device_context.vk_device_context().unwrap(), 155 | Some(raw_image), 156 | &RafxTextureDef { 157 | extents: RafxExtents3D { 158 | width: extents.width, 159 | height: extents.height, 160 | depth: 1, 161 | }, 162 | format, 163 | resource_type: RafxResourceType::TEXTURE, 164 | sample_count: RafxSampleCount::SampleCount1, 165 | ..Default::default() 166 | }, 167 | )?; 168 | 169 | let image = resource_manager 170 | .resources() 171 | .insert_image(RafxTexture::Vk(image)); 172 | let image_view = resource_manager 173 | .resources() 174 | .get_or_create_image_view(&image, None)?; 175 | 176 | Ok(VkSkiaSurface { 177 | device_context: device_context.clone(), 178 | surface, 179 | texture, 180 | image_view, 181 | }) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Skia + Vulkan = Skulpin 2 | //! 3 | //! This crate provides an easy option for drawing hardware-accelerated 2D by combining vulkan and 4 | //! skia. 5 | //! 6 | //! Two windowing backends are supported out of the box - winit and sdl2. 7 | //! 8 | //! Currently there are two ways to use this library. 9 | //! 10 | //! # skulpin::App 11 | //! 12 | //! Implement the AppHandler trait and launch the app. It's simple but not as flexible as using 13 | //! the renderer directly and handling the window manually. 14 | //! 15 | //! Utility classes are provided that make handling input and measuring time easier. 16 | //! 17 | //! # skulpin::Renderer 18 | //! 19 | //! You manage the window and event loop yourself. Then add the renderer to draw to it. 20 | //! 21 | //! This is the most flexible way to use the library 22 | //! 23 | //! 24 | 25 | // Export these crates so that downstream crates can easily use the same version of them as we do 26 | pub use skulpin_renderer::rafx; 27 | pub use skulpin_renderer::skia_safe; 28 | pub use skulpin_renderer::skia_bindings; 29 | 30 | pub use skulpin_renderer::RendererBuilder; 31 | pub use skulpin_renderer::Renderer; 32 | pub use skulpin_renderer::CoordinateSystemHelper; 33 | pub use skulpin_renderer::CoordinateSystem; 34 | pub use skulpin_renderer::ValidationMode; 35 | pub use skulpin_renderer::Size; 36 | pub use skulpin_renderer::LogicalSize; 37 | pub use skulpin_renderer::PhysicalSize; 38 | 39 | #[cfg(feature = "winit-app")] 40 | pub use skulpin_app_winit as app; 41 | #[cfg(feature = "winit-app")] 42 | pub use skulpin_app_winit::winit; 43 | --------------------------------------------------------------------------------