├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml └── workflows │ └── build.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── core ├── Cargo.toml ├── cbindgen.toml ├── latencyflex2.h └── src │ ├── dx12 │ ├── entrypoint.rs │ └── mod.rs │ ├── entrypoint.rs │ ├── ewma.rs │ ├── fence_worker.rs │ ├── lib.rs │ ├── profiler.rs │ ├── time │ ├── mod.rs │ ├── unix.rs │ └── windows.rs │ └── vulkan │ ├── entrypoint.rs │ └── mod.rs ├── docs ├── .gitignore ├── .vitepress │ ├── config.js │ └── theme │ │ ├── custom.css │ │ └── index.js ├── ea.md ├── index.md ├── public │ └── files │ │ └── EnableSignatureOverride.reg └── shim │ ├── building.md │ └── installing.md ├── package.json └── pnpm-lock.yaml /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "Title" 4 | labels: ["bug"] 5 | body: 6 | - type: input 7 | id: lfx-commit 8 | attributes: 9 | label: LFX2 commit 10 | description: Which commit of https://github.com/ishitatsuyuki/latencyflex2 are you running? 11 | validations: 12 | required: true 13 | - type: input 14 | id: dxvk-commit 15 | attributes: 16 | label: DXVK commit 17 | description: Which commit of https://github.com/ishitatsuyuki/dxvk are you running? 18 | validations: 19 | required: true 20 | - type: input 21 | id: nvapi-commit 22 | attributes: 23 | label: NVAPI commit 24 | description: Which commit of https://github.com/ishitatsuyuki/dxvk-nvapi are you running? 25 | validations: 26 | required: true 27 | - type: input 28 | id: vkd3d-commit 29 | attributes: 30 | label: VKD3D-Proton commit 31 | description: Which commit of https://github.com/ishitatsuyuki/vkd3d-proton are you running? (DX12 games only) 32 | - type: input 33 | id: game 34 | attributes: 35 | label: Game 36 | description: Game name (if relevant) 37 | - type: textarea 38 | id: description 39 | attributes: 40 | label: Issue Description 41 | description: | 42 | Describe the issue you're seeing below. 43 | Include log files (PROTON_LOG, Lutris logs, etc.). 44 | For frame time issues, please compress and upload the profile files (e.g. lfx2.2023.01.01-00.00.00.json) from your game folder. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | name: Rust ${{ matrix.os }} ${{ matrix.rust }} 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | rust: 15 | - stable 16 | - beta 17 | - nightly 18 | os: [ubuntu-latest, windows-latest] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Setup Rust 24 | uses: dtolnay/rust-toolchain@master 25 | with: 26 | toolchain: ${{ matrix.rust }} 27 | - name: Build 28 | run: cargo build --release 29 | working-directory: ./core 30 | - name: Test 31 | run: cargo test --release 32 | working-directory: ./core -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /target 3 | /Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "core" 4 | ] 5 | 6 | [profile.dev] 7 | panic = "abort" 8 | 9 | [profile.release] 10 | panic = "abort" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LatencyFleX 2 2 | 3 | LatencyFleX 2 is currently under heavy development. 4 | 5 | See [documentation](https://lfx2.ishitatsuy.uk/ea.html) for early access disclaimer and installation instructions. -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "latencyflex2-rust" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | chrono = "0.4.23" 13 | once_cell = "1.16.0" 14 | parking_lot = "0.12.1" 15 | spark = { git = "https://github.com/sjb3d/spark.git", optional = true } 16 | 17 | [target.'cfg(unix)'.dependencies] 18 | nix = { version = "0.26.1", default-features = false, features = ["time"] } 19 | 20 | [target.'cfg(windows)'.dependencies.windows] 21 | version = "0.43.0" 22 | features = ["Win32_Foundation", "Win32_System_Threading", "Win32_System_Performance", "Win32_Security", "Win32_System_WindowsProgramming", 23 | "Win32_System_LibraryLoader"] 24 | 25 | [features] 26 | default = ["dx12", "vulkan"] 27 | dx12 = ["windows/Win32_Graphics_Direct3D12", "windows/Win32_Graphics_Dxgi_Common"] 28 | vulkan = ["spark"] 29 | -------------------------------------------------------------------------------- /core/cbindgen.toml: -------------------------------------------------------------------------------- 1 | # This is a template cbindgen.toml file with all of the default values. 2 | # Some values are commented out because their absence is the real default. 3 | # 4 | # See https://github.com/eqrion/cbindgen/blob/master/docs.md#cbindgentoml 5 | # for detailed documentation of every option here. 6 | 7 | 8 | 9 | language = "C" 10 | cpp_compat = true 11 | 12 | 13 | 14 | ############## Options for Wrapping the Contents of the Header ################# 15 | 16 | # header = "/* Text to put at the beginning of the generated file. Probably a license. */" 17 | # trailer = "/* Text to put at the end of the generated file */" 18 | include_guard = "LATENCYFLEX2_H" 19 | # pragma_once = true 20 | # autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" 21 | include_version = false 22 | # namespace = "my_namespace" 23 | namespaces = [] 24 | using_namespaces = [] 25 | sys_includes = [] 26 | includes = [] 27 | no_includes = false 28 | after_includes = """ 29 | #ifdef LFX2_VK 30 | #include 31 | #endif 32 | 33 | #ifdef LFX2_DX12 34 | #include 35 | #endif 36 | 37 | #ifdef _WIN32 38 | #define LFX2_API __declspec(dllimport) 39 | #else 40 | #define LFX2_API 41 | #endif 42 | """ 43 | 44 | 45 | 46 | 47 | ############################ Code Style Options ################################ 48 | 49 | braces = "SameLine" 50 | line_length = 100 51 | tab_width = 2 52 | documentation = true 53 | documentation_style = "auto" 54 | documentation_length = "full" 55 | line_endings = "LF" # also "CR", "CRLF", "Native" 56 | 57 | 58 | 59 | 60 | ############################# Codegen Options ################################## 61 | 62 | style = "both" 63 | sort_by = "None" # default for `fn.sort_by` and `const.sort_by` 64 | usize_is_size_t = true 65 | 66 | 67 | 68 | [defines] 69 | "target_os = windows" = "_WIN32" 70 | "feature = dx12" = "LFX2_DX12" 71 | "feature = vulkan" = "LFX2_VK" 72 | 73 | 74 | [export] 75 | include = [] 76 | exclude = [] 77 | prefix = "lfx2" 78 | item_types = [] 79 | renaming_overrides_prefixing = true 80 | 81 | 82 | 83 | [export.rename] 84 | "ID3D12Device" = "ID3D12Device*" 85 | "ID3D12CommandQueue" = "ID3D12CommandQueue*" 86 | "Device" = "VkDevice" 87 | "Instance" = "VkInstance" 88 | "PhysicalDevice" = "VkPhysicalDevice" 89 | "CommandBuffer" = "VkCommandBuffer" 90 | 91 | [export.body] 92 | 93 | 94 | [export.mangle] 95 | 96 | 97 | [fn] 98 | rename_args = "None" 99 | # must_use = "MUST_USE_FUNC" 100 | # no_return = "NO_RETURN" 101 | prefix = "LFX2_API" 102 | # postfix = "END_FUNC" 103 | args = "auto" 104 | sort_by = "None" 105 | 106 | 107 | 108 | 109 | [struct] 110 | rename_fields = "None" 111 | # must_use = "MUST_USE_STRUCT" 112 | derive_constructor = false 113 | derive_eq = false 114 | derive_neq = false 115 | derive_lt = false 116 | derive_lte = false 117 | derive_gt = false 118 | derive_gte = false 119 | 120 | 121 | 122 | 123 | [enum] 124 | rename_variants = "CamelCase" 125 | # must_use = "MUST_USE_ENUM" 126 | add_sentinel = false 127 | prefix_with_name = true 128 | derive_helper_methods = false 129 | derive_const_casts = false 130 | derive_mut_casts = false 131 | # cast_assert_name = "ASSERT" 132 | derive_tagged_enum_destructor = false 133 | derive_tagged_enum_copy_constructor = false 134 | enum_class = true 135 | private_default_tagged_enum_constructor = false 136 | 137 | 138 | 139 | 140 | [const] 141 | allow_static_const = true 142 | allow_constexpr = false 143 | sort_by = "None" 144 | 145 | 146 | 147 | 148 | [macro_expansion] 149 | bitflags = false 150 | 151 | 152 | 153 | 154 | 155 | 156 | ############## Options for How Your Rust library Should Be Parsed ############## 157 | 158 | [parse] 159 | parse_deps = false 160 | # include = [] 161 | exclude = [] 162 | clean = false 163 | extra_bindings = [] 164 | 165 | 166 | 167 | [parse.expand] 168 | crates = [] 169 | all_features = false 170 | default_features = true 171 | features = [] 172 | -------------------------------------------------------------------------------- /core/latencyflex2.h: -------------------------------------------------------------------------------- 1 | #ifndef LATENCYFLEX2_H 2 | #define LATENCYFLEX2_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #ifdef LFX2_VK 10 | #include 11 | #endif 12 | 13 | #ifdef LFX2_DX12 14 | #include 15 | #endif 16 | 17 | #ifdef _WIN32 18 | #define LFX2_API __declspec(dllimport) 19 | #else 20 | #define LFX2_API 21 | #endif 22 | 23 | typedef enum lfx2MarkType { 24 | lfx2MarkTypeBegin, 25 | lfx2MarkTypeEnd, 26 | } lfx2MarkType; 27 | 28 | typedef struct lfx2Context lfx2Context; 29 | 30 | #if (defined(LFX2_DX12) && defined(_WIN32)) 31 | typedef struct lfx2Dx12Context lfx2Dx12Context; 32 | #endif 33 | 34 | /** 35 | * A write handle for frame markers. 36 | */ 37 | typedef struct lfx2Frame lfx2Frame; 38 | 39 | typedef struct lfx2ImplicitContext lfx2ImplicitContext; 40 | 41 | #if defined(LFX2_VK) 42 | typedef struct lfx2VulkanContext lfx2VulkanContext; 43 | #endif 44 | 45 | #if (defined(LFX2_DX12) && defined(_WIN32)) 46 | typedef struct lfx2Dx12SubmitAux { 47 | ID3D12GraphicsCommandList* execute_before; 48 | ID3D12GraphicsCommandList* execute_after; 49 | ID3D12Fence* signal_fence; 50 | uint64_t signal_fence_value; 51 | } lfx2Dx12SubmitAux; 52 | #endif 53 | 54 | typedef uint64_t lfx2Timestamp; 55 | typedef uint64_t lfx2Interval; 56 | 57 | typedef uint32_t lfx2SectionId; 58 | 59 | #if defined(LFX2_VK) 60 | typedef struct lfx2VulkanSubmitAux { 61 | VkCommandBuffer submit_before; 62 | VkCommandBuffer submit_after; 63 | VkSemaphore signal_sem; 64 | uint64_t signal_sem_value; 65 | } lfx2VulkanSubmitAux; 66 | #endif 67 | 68 | #ifdef __cplusplus 69 | extern "C" { 70 | #endif // __cplusplus 71 | 72 | #if (defined(LFX2_DX12) && defined(_WIN32)) 73 | LFX2_API struct lfx2Dx12Context *lfx2Dx12ContextCreate(ID3D12Device* device); 74 | 75 | LFX2_API void lfx2Dx12ContextAddRef(struct lfx2Dx12Context *context); 76 | 77 | LFX2_API void lfx2Dx12ContextRelease(struct lfx2Dx12Context *context); 78 | 79 | LFX2_API 80 | struct lfx2Dx12SubmitAux lfx2Dx12ContextBeforeSubmit(struct lfx2Dx12Context *context, 81 | ID3D12CommandQueue* queue); 82 | 83 | LFX2_API void lfx2Dx12ContextBeginFrame(struct lfx2Dx12Context *context, struct lfx2Frame *frame); 84 | 85 | LFX2_API void lfx2Dx12ContextEndFrame(struct lfx2Dx12Context *context, struct lfx2Frame *frame); 86 | #endif 87 | 88 | LFX2_API lfx2Timestamp lfx2TimestampNow(void); 89 | 90 | #if defined(_WIN32) 91 | LFX2_API lfx2Timestamp lfx2TimestampFromQpc(uint64_t qpc); 92 | #endif 93 | 94 | LFX2_API void lfx2SleepUntil(lfx2Timestamp target); 95 | 96 | LFX2_API struct lfx2Context *lfx2ContextCreate(void); 97 | 98 | LFX2_API void lfx2ContextAddRef(struct lfx2Context *context); 99 | 100 | LFX2_API void lfx2ContextRelease(struct lfx2Context *context); 101 | 102 | LFX2_API 103 | struct lfx2Frame *lfx2FrameCreate(struct lfx2Context *context, 104 | lfx2Timestamp *out_timestamp); 105 | 106 | LFX2_API void lfx2FrameAddRef(struct lfx2Frame *frame); 107 | 108 | LFX2_API void lfx2FrameRelease(struct lfx2Frame *frame); 109 | 110 | LFX2_API 111 | void lfx2MarkSection(struct lfx2Frame *frame, 112 | lfx2SectionId section_id, 113 | enum lfx2MarkType mark_type, 114 | lfx2Timestamp timestamp); 115 | 116 | LFX2_API 117 | void lfx2FrameOverrideQueuingDelay(struct lfx2Frame *frame, 118 | lfx2SectionId section_id, 119 | lfx2Interval queueing_delay); 120 | 121 | LFX2_API 122 | void lfx2FrameOverrideInverseThroughput(struct lfx2Frame *frame, 123 | lfx2SectionId section_id, 124 | lfx2Interval inverse_throughput); 125 | 126 | LFX2_API struct lfx2ImplicitContext *lfx2ImplicitContextCreate(void); 127 | 128 | LFX2_API void lfx2ImplicitContextRelease(struct lfx2ImplicitContext *context); 129 | 130 | LFX2_API void lfx2ImplicitContextReset(struct lfx2ImplicitContext *context); 131 | 132 | LFX2_API 133 | struct lfx2Frame *lfx2FrameCreateImplicit(struct lfx2ImplicitContext *context, 134 | lfx2Timestamp *out_timestamp); 135 | 136 | LFX2_API 137 | struct lfx2Frame *lfx2FrameDequeueImplicit(struct lfx2ImplicitContext *context, 138 | bool critical); 139 | 140 | #if defined(LFX2_VK) 141 | LFX2_API 142 | struct lfx2VulkanContext *lfx2VulkanContextCreate(PFN_vkGetInstanceProcAddr gipa, 143 | VkInstance instance, 144 | VkPhysicalDevice physical_device, 145 | VkDevice device, 146 | uint32_t queue_family_index); 147 | 148 | LFX2_API void lfx2VulkanContextAddRef(struct lfx2VulkanContext *context); 149 | 150 | LFX2_API void lfx2VulkanContextRelease(struct lfx2VulkanContext *context); 151 | 152 | LFX2_API 153 | struct lfx2VulkanSubmitAux lfx2VulkanContextBeforeSubmit(struct lfx2VulkanContext *context); 154 | 155 | LFX2_API 156 | void lfx2VulkanContextBeginFrame(struct lfx2VulkanContext *context, 157 | struct lfx2Frame *frame); 158 | 159 | LFX2_API void lfx2VulkanContextEndFrame(struct lfx2VulkanContext *context, struct lfx2Frame *frame); 160 | #endif 161 | 162 | #ifdef __cplusplus 163 | } // extern "C" 164 | #endif // __cplusplus 165 | 166 | #endif /* LATENCYFLEX2_H */ 167 | -------------------------------------------------------------------------------- /core/src/dx12/entrypoint.rs: -------------------------------------------------------------------------------- 1 | use crate::dx12::{Dx12Context, Dx12SubmitAux}; 2 | use crate::time::timestamp_now; 3 | use crate::{Frame, MarkType}; 4 | use std::mem::ManuallyDrop; 5 | use std::sync::Arc; 6 | use windows::Win32::Graphics::Direct3D12::{ID3D12CommandQueue, ID3D12Device}; 7 | 8 | #[no_mangle] 9 | pub unsafe extern "C" fn lfx2Dx12ContextCreate( 10 | device: ManuallyDrop, 11 | ) -> *mut Dx12Context { 12 | let context = Dx12Context::new(&device); 13 | Arc::into_raw(context) as _ 14 | } 15 | 16 | #[no_mangle] 17 | pub unsafe extern "C" fn lfx2Dx12ContextAddRef(context: *mut Dx12Context) { 18 | Arc::increment_strong_count(context); 19 | } 20 | 21 | #[no_mangle] 22 | pub unsafe extern "C" fn lfx2Dx12ContextRelease(context: *mut Dx12Context) { 23 | Arc::decrement_strong_count(context); 24 | } 25 | 26 | #[no_mangle] 27 | pub unsafe extern "C" fn lfx2Dx12ContextBeforeSubmit( 28 | context: *mut Dx12Context, 29 | queue: ManuallyDrop, 30 | ) -> Dx12SubmitAux { 31 | (*context).inner.lock().submit(&queue) 32 | } 33 | 34 | #[no_mangle] 35 | pub unsafe extern "C" fn lfx2Dx12ContextBeginFrame(context: *mut Dx12Context, frame: *mut Frame) { 36 | let frame = Arc::from_raw(frame); 37 | frame.mark(800, MarkType::Begin, timestamp_now()); 38 | (*context).inner.lock().begin(&frame); 39 | let _ = Arc::into_raw(frame); 40 | } 41 | 42 | #[no_mangle] 43 | pub unsafe extern "C" fn lfx2Dx12ContextEndFrame(context: *mut Dx12Context, frame: *mut Frame) { 44 | let frame = Arc::from_raw(frame); 45 | (*context).inner.lock().end(&frame); 46 | frame.mark(800, MarkType::End, timestamp_now()); 47 | let _ = Arc::into_raw(frame); 48 | } 49 | -------------------------------------------------------------------------------- /core/src/dx12/mod.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | use std::sync::{mpsc, Arc, Weak}; 3 | use std::thread::JoinHandle; 4 | use std::{ptr, thread}; 5 | 6 | use parking_lot::Mutex; 7 | use windows::core::Interface; 8 | use windows::Win32::Foundation::{CloseHandle, HANDLE}; 9 | use windows::Win32::Graphics::Direct3D12::*; 10 | use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_UNKNOWN, DXGI_SAMPLE_DESC}; 11 | use windows::Win32::System::Threading::{CreateEventW, WaitForSingleObject}; 12 | use windows::Win32::System::WindowsProgramming::INFINITE; 13 | 14 | use crate::{timestamp_from_qpc, Frame, Interval, MarkType, Timestamp}; 15 | 16 | pub mod entrypoint; 17 | 18 | pub struct Dx12Context { 19 | inner: Mutex, 20 | } 21 | 22 | struct Dx12ContextInner { 23 | device: ID3D12Device4, 24 | timestamp_command_list: [ID3D12GraphicsCommandList; 2], 25 | command_allocator: Vec, 26 | query_heap: ID3D12QueryHeap, 27 | query_staging: Vec<(ID3D12Resource, u32)>, 28 | 29 | // We expect the application to hold a reference to the frame until the call the "end" marker, 30 | // where we'll finalize and set this to None as well. 31 | // If the application releases the reference without calling the "end" marker, we just skip 32 | // processing for events happened during that. 33 | current_frame: Option>, 34 | current_frame_begun: bool, 35 | 36 | fence_thread: Option>, 37 | fence_tx: Option>, 38 | 39 | fence: ID3D12Fence, 40 | fence_value: u64, 41 | } 42 | 43 | #[repr(C)] 44 | #[derive(Default)] 45 | pub struct Dx12SubmitAux { 46 | execute_before: Option, 47 | execute_after: Option, 48 | signal_fence: Option, 49 | signal_fence_value: u64, 50 | } 51 | 52 | impl Dx12Context { 53 | pub fn new(device: &ID3D12Device) -> Arc { 54 | let device = device.cast::().unwrap(); 55 | let context = Arc::new(Dx12Context { 56 | inner: Mutex::new(Dx12ContextInner::new(device)), 57 | }); 58 | context 59 | .inner 60 | .lock() 61 | .fence_tx 62 | .as_mut() 63 | .unwrap() 64 | .send(Dx12FenceMsg::SetContext(Arc::downgrade(&context))) 65 | .unwrap(); 66 | context 67 | } 68 | } 69 | 70 | impl Dx12ContextInner { 71 | fn new(device: ID3D12Device4) -> Dx12ContextInner { 72 | let query_heap = unsafe { 73 | let mut query_heap = MaybeUninit::uninit(); 74 | device 75 | .CreateQueryHeap( 76 | &D3D12_QUERY_HEAP_DESC { 77 | Type: D3D12_QUERY_HEAP_TYPE_TIMESTAMP, 78 | Count: 2, 79 | NodeMask: 0, 80 | }, 81 | query_heap.as_mut_ptr(), 82 | ) 83 | .unwrap(); 84 | query_heap.assume_init().unwrap() 85 | }; 86 | 87 | let timestamp_command_list = [(); 2].map(|_| unsafe { 88 | device 89 | .CreateCommandList1( 90 | 0, 91 | D3D12_COMMAND_LIST_TYPE_DIRECT, 92 | D3D12_COMMAND_LIST_FLAG_NONE, 93 | ) 94 | .unwrap() 95 | }); 96 | 97 | let fence: ID3D12Fence = unsafe { device.CreateFence(0, D3D12_FENCE_FLAG_NONE).unwrap() }; 98 | 99 | let event = unsafe { CreateEventW(None, false, false, None).unwrap() }; 100 | 101 | let (fence_tx, fence_rx) = mpsc::channel(); 102 | let mut fence_thread_ctx = Dx12FenceWorker { 103 | context: None, 104 | rx: fence_rx, 105 | tracker: None, 106 | fence: fence.clone(), 107 | event, 108 | }; 109 | let fence_thread = thread::spawn(move || fence_thread_ctx.run()); 110 | 111 | Dx12ContextInner { 112 | device, 113 | timestamp_command_list, 114 | command_allocator: vec![], 115 | query_heap, 116 | query_staging: vec![], 117 | current_frame: None, 118 | current_frame_begun: false, 119 | fence_thread: Some(fence_thread), 120 | fence_tx: Some(fence_tx), 121 | fence, 122 | fence_value: 1, 123 | } 124 | } 125 | } 126 | 127 | impl Drop for Dx12ContextInner { 128 | fn drop(&mut self) { 129 | let _ = self.fence_tx.take(); 130 | self.fence_thread.take().unwrap().join().unwrap(); 131 | } 132 | } 133 | 134 | impl Dx12ContextInner { 135 | fn get_query(&mut self) -> (ID3D12Resource, u32) { 136 | if let Some(q) = self.query_staging.pop() { 137 | q 138 | } else { 139 | let count = 16; 140 | let resource_desc = D3D12_RESOURCE_DESC { 141 | Dimension: D3D12_RESOURCE_DIMENSION_BUFFER, 142 | Alignment: 0, 143 | Width: (count * 8) as u64, 144 | Height: 1, 145 | DepthOrArraySize: 1, 146 | MipLevels: 1, 147 | Format: DXGI_FORMAT_UNKNOWN, 148 | SampleDesc: DXGI_SAMPLE_DESC { 149 | Count: 1, 150 | Quality: 0, 151 | }, 152 | Layout: D3D12_TEXTURE_LAYOUT_ROW_MAJOR, 153 | Flags: D3D12_RESOURCE_FLAG_NONE, 154 | }; 155 | let heap_properties = D3D12_HEAP_PROPERTIES { 156 | Type: D3D12_HEAP_TYPE_READBACK, 157 | ..Default::default() 158 | }; 159 | let buf: ID3D12Resource = unsafe { 160 | let mut buf = MaybeUninit::uninit(); 161 | self.device 162 | .CreateCommittedResource( 163 | &heap_properties, 164 | D3D12_HEAP_FLAG_NONE, 165 | &resource_desc, 166 | D3D12_RESOURCE_STATE_COPY_DEST, 167 | None, 168 | buf.as_mut_ptr(), 169 | ) 170 | .unwrap(); 171 | buf.assume_init().unwrap() 172 | }; 173 | unsafe { 174 | buf.Map(0, None, None).unwrap(); 175 | } 176 | for i in 0..count { 177 | self.query_staging.push((buf.clone(), i)); 178 | } 179 | self.query_staging.pop().unwrap() 180 | } 181 | } 182 | 183 | fn get_allocator(&mut self) -> ID3D12CommandAllocator { 184 | if let Some(a) = self.command_allocator.pop() { 185 | a 186 | } else { 187 | unsafe { 188 | self.device 189 | .CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT) 190 | .unwrap() 191 | } 192 | } 193 | } 194 | 195 | fn begin(&mut self, frame: &Arc) { 196 | let weak = Arc::downgrade(frame); 197 | self.current_frame = Some(weak.clone()); 198 | self.current_frame_begun = false; 199 | 200 | self.fence_tx 201 | .as_mut() 202 | .unwrap() 203 | .send(Dx12FenceMsg::BeginFrame(weak)) 204 | .unwrap(); 205 | } 206 | 207 | fn end(&mut self, frame: &Arc) { 208 | self.current_frame = None; 209 | 210 | self.fence_tx 211 | .as_mut() 212 | .unwrap() 213 | .send(Dx12FenceMsg::EndFrame(frame.clone())) 214 | .unwrap(); 215 | } 216 | 217 | fn submit(&mut self, queue: &ID3D12CommandQueue) -> Dx12SubmitAux { 218 | if self.current_frame.is_none() { 219 | return Dx12SubmitAux::default(); 220 | } 221 | 222 | let allocator = self.get_allocator(); 223 | 224 | let build_timestamp_command_list = |command_list: &ID3D12GraphicsCommandList, 225 | query_heap: (&ID3D12QueryHeap, u32), 226 | staging: &(ID3D12Resource, u32)| 227 | -> windows::core::Result<()> { 228 | unsafe { 229 | command_list.Reset(&allocator, None)?; 230 | command_list.EndQuery(query_heap.0, D3D12_QUERY_TYPE_TIMESTAMP, query_heap.1); 231 | command_list.ResolveQueryData( 232 | query_heap.0, 233 | D3D12_QUERY_TYPE_TIMESTAMP, 234 | query_heap.1, 235 | 1, 236 | &staging.0, 237 | (staging.1 * 8) as u64, 238 | ); 239 | command_list.Close()?; 240 | }; 241 | Ok(()) 242 | }; 243 | 244 | let (execute_before, begin_query) = if !self.current_frame_begun { 245 | let query = self.get_query(); 246 | build_timestamp_command_list( 247 | &self.timestamp_command_list[0], 248 | (&self.query_heap, 0), 249 | &query, 250 | ) 251 | .unwrap(); 252 | self.current_frame_begun = true; 253 | (Some(self.timestamp_command_list[0].clone()), Some(query)) 254 | } else { 255 | (None, None) 256 | }; 257 | 258 | let end_query = self.get_query(); 259 | build_timestamp_command_list( 260 | &self.timestamp_command_list[1], 261 | (&self.query_heap, 1), 262 | &end_query, 263 | ) 264 | .unwrap(); 265 | 266 | let fence_value = self.fence_value; 267 | self.fence_value += 1; 268 | 269 | self.fence_tx 270 | .as_mut() 271 | .unwrap() 272 | .send(Dx12FenceMsg::Wait(Dx12FenceWait { 273 | queue: queue.clone(), 274 | value: fence_value, 275 | allocator, 276 | begin_ts: begin_query, 277 | end_ts: end_query, 278 | })) 279 | .unwrap(); 280 | 281 | Dx12SubmitAux { 282 | execute_before, 283 | execute_after: Some(self.timestamp_command_list[1].clone()), 284 | signal_fence: Some(self.fence.clone()), 285 | signal_fence_value: fence_value, 286 | } 287 | } 288 | } 289 | 290 | struct Dx12FenceWait { 291 | value: u64, 292 | queue: ID3D12CommandQueue, 293 | allocator: ID3D12CommandAllocator, 294 | begin_ts: Option<(ID3D12Resource, u32)>, 295 | end_ts: (ID3D12Resource, u32), 296 | } 297 | 298 | enum Dx12FenceMsg { 299 | SetContext(Weak), 300 | BeginFrame(Weak), 301 | Wait(Dx12FenceWait), 302 | EndFrame(Arc), 303 | } 304 | 305 | struct Dx12FenceWorker { 306 | context: Option>, 307 | rx: mpsc::Receiver, 308 | 309 | tracker: Option, 310 | 311 | fence: ID3D12Fence, 312 | event: HANDLE, 313 | } 314 | 315 | struct Dx12FenceWorkerTracker { 316 | frame: Weak, 317 | end_ts: Option, 318 | queuing_delay: Interval, 319 | } 320 | 321 | impl Dx12FenceWorker { 322 | fn run(&mut self) { 323 | while let Ok(job) = self.rx.recv() { 324 | match job { 325 | Dx12FenceMsg::SetContext(ctx) => { 326 | self.context = Some(ctx); 327 | } 328 | Dx12FenceMsg::BeginFrame(frame) => { 329 | self.tracker = Some(Dx12FenceWorkerTracker { 330 | frame, 331 | end_ts: None, 332 | queuing_delay: u64::MAX, 333 | }); 334 | } 335 | Dx12FenceMsg::Wait(job) => { 336 | self.process_fence_wait(job).unwrap(); 337 | } 338 | Dx12FenceMsg::EndFrame(frame) => { 339 | self.process_end_frame(frame); 340 | } 341 | } 342 | } 343 | } 344 | 345 | fn process_fence_wait(&mut self, job: Dx12FenceWait) -> windows::core::Result<()> { 346 | unsafe { 347 | self.fence.SetEventOnCompletion(job.value, self.event)?; 348 | WaitForSingleObject(self.event, INFINITE).ok()?; 349 | job.allocator.Reset()?; 350 | } 351 | 352 | let mut gpu_calibration = 0; 353 | let mut cpu_qpc = 0; 354 | let timestamp_frequency; 355 | unsafe { 356 | job.queue 357 | .GetClockCalibration(&mut gpu_calibration, &mut cpu_qpc)?; 358 | timestamp_frequency = job.queue.GetTimestampFrequency()?; 359 | } 360 | let cpu_calibration = timestamp_from_qpc(cpu_qpc); 361 | 362 | let context = self.context.as_mut().and_then(|weak| weak.upgrade()); 363 | let context = match context { 364 | Some(context) => context, 365 | None => return Ok(()), 366 | }; 367 | let mut context = context.inner.lock(); 368 | context.command_allocator.push(job.allocator); 369 | 370 | let process_timestamp = 371 | |(buf, index): &(ID3D12Resource, u32)| -> windows::core::Result { 372 | let gpu_ts = unsafe { 373 | let mut base = ptr::null_mut(); 374 | buf.Map(0, None, Some(&mut base as _))?; 375 | let ret = ptr::read((base as *const u64).add(*index as usize)); 376 | buf.Unmap(0, None); 377 | ret 378 | }; 379 | let gpu_delta = gpu_ts as i64 - gpu_calibration as i64; 380 | let calibrated = 381 | cpu_calibration as i64 + gpu_delta * 1_000_000_000 / timestamp_frequency as i64; 382 | Ok(calibrated as u64) 383 | }; 384 | 385 | // The application might end up mismatching call pairs, fail gracefully in such cases. 386 | let mut tracker = self.tracker.as_mut(); 387 | 388 | if let Some(res) = job.begin_ts { 389 | let begin = process_timestamp(&res)?; 390 | if let Some(frame) = tracker.as_mut().and_then(|t| t.frame.upgrade()) { 391 | frame.mark(1000, MarkType::Begin, begin); 392 | } 393 | 394 | context.query_staging.push(res); 395 | } 396 | if let Some(tracker) = &mut tracker { 397 | tracker.end_ts = Some(process_timestamp(&job.end_ts)?); 398 | } 399 | context.query_staging.push(job.end_ts); 400 | 401 | Ok(()) 402 | } 403 | 404 | fn process_end_frame(&mut self, frame: Arc) { 405 | let tracker = self.tracker.take().unwrap(); 406 | assert_eq!(Arc::as_ptr(&frame), Weak::as_ptr(&tracker.frame)); 407 | if let Some(end_ts) = tracker.end_ts { 408 | frame.mark(1000, MarkType::End, end_ts); 409 | } 410 | // TODO: queueing delay 411 | } 412 | } 413 | 414 | impl Drop for Dx12FenceWorker { 415 | fn drop(&mut self) { 416 | unsafe { 417 | CloseHandle(self.event); 418 | } 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /core/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | use crate::time::{sleep_until, timestamp_now}; 2 | use crate::{Context, Frame, ImplicitContext, Interval, MarkType, SectionId, Timestamp}; 3 | use std::ptr::NonNull; 4 | use std::sync::Arc; 5 | 6 | #[no_mangle] 7 | pub unsafe extern "C" fn lfx2TimestampNow() -> Timestamp { 8 | timestamp_now() 9 | } 10 | 11 | #[cfg(target_os = "windows")] 12 | #[no_mangle] 13 | pub unsafe extern "C" fn lfx2TimestampFromQpc(qpc: u64) -> Timestamp { 14 | use crate::timestamp_from_qpc; 15 | timestamp_from_qpc(qpc) 16 | } 17 | 18 | #[no_mangle] 19 | pub unsafe extern "C" fn lfx2SleepUntil(target: Timestamp) { 20 | sleep_until(target) 21 | } 22 | 23 | #[no_mangle] 24 | pub unsafe extern "C" fn lfx2ContextCreate() -> *mut Context { 25 | Arc::into_raw(Arc::new(Context::default())) as _ 26 | } 27 | 28 | #[no_mangle] 29 | pub unsafe extern "C" fn lfx2ContextAddRef(context: *mut Context) { 30 | Arc::increment_strong_count(context); 31 | } 32 | 33 | #[no_mangle] 34 | pub unsafe extern "C" fn lfx2ContextRelease(context: *mut Context) { 35 | Arc::decrement_strong_count(context); 36 | } 37 | 38 | #[no_mangle] 39 | pub unsafe extern "C" fn lfx2FrameCreate( 40 | context: *mut Context, 41 | out_timestamp: *mut Timestamp, 42 | ) -> *mut Frame { 43 | let context = Arc::from_raw(context); 44 | let (frame, timestamp) = context.inner.lock().prepare_frame(context.clone()); 45 | *out_timestamp = timestamp; 46 | let _ = Arc::into_raw(context); 47 | Arc::into_raw(frame) as _ 48 | } 49 | 50 | #[no_mangle] 51 | pub unsafe extern "C" fn lfx2FrameAddRef(frame: *mut Frame) { 52 | Arc::increment_strong_count(frame); 53 | } 54 | 55 | #[no_mangle] 56 | pub unsafe extern "C" fn lfx2FrameRelease(frame: *mut Frame) { 57 | Arc::decrement_strong_count(frame); 58 | } 59 | 60 | #[no_mangle] 61 | pub unsafe extern "C" fn lfx2MarkSection( 62 | frame: *mut Frame, 63 | section_id: SectionId, 64 | mark_type: MarkType, 65 | timestamp: Timestamp, 66 | ) { 67 | (*frame).mark(section_id, mark_type, timestamp); 68 | } 69 | 70 | #[no_mangle] 71 | pub unsafe extern "C" fn lfx2FrameOverrideQueuingDelay( 72 | frame: *mut Frame, 73 | section_id: SectionId, 74 | queueing_delay: Interval, 75 | ) { 76 | (*frame).set_queueing_delay(section_id, queueing_delay); 77 | } 78 | 79 | #[no_mangle] 80 | pub unsafe extern "C" fn lfx2FrameOverrideInverseThroughput( 81 | frame: *mut Frame, 82 | section_id: SectionId, 83 | inverse_throughput: Interval, 84 | ) { 85 | (*frame).set_inv_throughput(section_id, inverse_throughput); 86 | } 87 | 88 | #[no_mangle] 89 | pub unsafe extern "C" fn lfx2ImplicitContextCreate() -> *mut ImplicitContext { 90 | let context = Box::new(ImplicitContext::default()); 91 | Box::into_raw(context) 92 | } 93 | 94 | #[no_mangle] 95 | pub unsafe extern "C" fn lfx2ImplicitContextRelease(context: *mut ImplicitContext) { 96 | let _ = Box::from_raw(context); 97 | } 98 | 99 | #[no_mangle] 100 | pub unsafe extern "C" fn lfx2ImplicitContextReset(context: *mut ImplicitContext) { 101 | (*context).reset(); 102 | } 103 | 104 | #[no_mangle] 105 | pub unsafe extern "C" fn lfx2FrameCreateImplicit( 106 | context: *mut ImplicitContext, 107 | out_timestamp: *mut Timestamp, 108 | ) -> *mut Frame { 109 | let (frame, timestamp) = (*context).enqueue(); 110 | *out_timestamp = timestamp; 111 | Arc::into_raw(frame) as _ 112 | } 113 | 114 | #[no_mangle] 115 | pub unsafe extern "C" fn lfx2FrameDequeueImplicit( 116 | context: *mut ImplicitContext, 117 | critical: bool, 118 | ) -> Option> { 119 | let frame = (*context).dequeue(critical); 120 | frame.map(|f| NonNull::new(Arc::into_raw(f) as _).unwrap()) 121 | } 122 | -------------------------------------------------------------------------------- /core/src/ewma.rs: -------------------------------------------------------------------------------- 1 | pub struct EwmaEstimator { 2 | current: f64, 3 | current_weight: f64, 4 | alpha: f64, 5 | } 6 | 7 | impl EwmaEstimator { 8 | pub fn new(alpha: f64) -> EwmaEstimator { 9 | EwmaEstimator { 10 | current: 0., 11 | current_weight: 0., 12 | alpha, 13 | } 14 | } 15 | 16 | pub fn update(&mut self, v: f64) { 17 | self.current = (1. - self.alpha) * self.current + self.alpha * v; 18 | self.current_weight = (1. - self.alpha) * self.current_weight + self.alpha; 19 | } 20 | 21 | pub fn get(&self) -> f64 { 22 | if self.current_weight == 0. { 23 | 0. 24 | } else { 25 | self.current / self.current_weight 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/fence_worker.rs: -------------------------------------------------------------------------------- 1 | use crate::{Frame, Interval, MarkType, Timestamp}; 2 | use std::sync::{mpsc, Arc, Weak}; 3 | 4 | pub struct FenceThread { 5 | thread: Option>, 6 | tx: Option>>, 7 | } 8 | 9 | impl FenceThread { 10 | pub fn new (Timestamp, Timestamp, Timestamp) + Send + 'static>( 11 | callback: F, 12 | ) -> Self { 13 | let (tx, rx) = mpsc::channel(); 14 | let mut worker = FenceWorker { 15 | rx, 16 | tracker: None, 17 | last_finish: 0, 18 | callback, 19 | }; 20 | let thread = std::thread::spawn(move || worker.run()); 21 | Self { 22 | thread: Some(thread), 23 | tx: Some(tx), 24 | } 25 | } 26 | 27 | pub fn send(&mut self, msg: FenceWorkerMessage) { 28 | self.tx.as_mut().unwrap().send(msg).unwrap(); 29 | } 30 | } 31 | 32 | impl Drop for FenceThread { 33 | fn drop(&mut self) { 34 | let _ = self.tx.take(); 35 | let _ = self.thread.take().unwrap().join(); 36 | } 37 | } 38 | 39 | struct FenceWorker (Timestamp, Timestamp, Timestamp)> { 40 | rx: mpsc::Receiver>, 41 | tracker: Option, 42 | 43 | last_finish: Timestamp, 44 | 45 | callback: F, 46 | } 47 | 48 | struct Tracker { 49 | frame: Weak, 50 | begin_ts: Option, 51 | end_ts: Option, 52 | duration: Interval, 53 | queuing_delay: Option, 54 | } 55 | 56 | pub enum FenceWorkerMessage { 57 | BeginFrame(Weak), 58 | Wait(S), 59 | EndFrame(Arc), 60 | } 61 | 62 | impl (Timestamp, Timestamp, Timestamp)> FenceWorker { 63 | fn run(&mut self) { 64 | while let Ok(job) = self.rx.recv() { 65 | match job { 66 | FenceWorkerMessage::BeginFrame(frame) => { 67 | self.tracker = Some(Tracker { 68 | frame, 69 | begin_ts: None, 70 | end_ts: None, 71 | queuing_delay: None, 72 | duration: 0, 73 | }); 74 | } 75 | FenceWorkerMessage::Wait(job) => { 76 | let (submission_ts, begin_ts, end_ts) = (self.callback)(job); 77 | if let Some(tr) = self.tracker.as_mut() { 78 | tr.begin_ts = 79 | Some(tr.begin_ts.map(|ts| ts.min(begin_ts)).unwrap_or(begin_ts)); 80 | tr.end_ts = Some(tr.end_ts.map(|ts| ts.max(end_ts)).unwrap_or(end_ts)); 81 | 82 | let queueing_delay = self.last_finish.saturating_sub(submission_ts); 83 | tr.queuing_delay = Some( 84 | tr.queuing_delay 85 | .map(|ts| ts.min(queueing_delay)) 86 | .unwrap_or(queueing_delay), 87 | ); 88 | 89 | let duration = end_ts.saturating_sub(self.last_finish.max(submission_ts)); 90 | tr.duration += duration; 91 | 92 | self.last_finish = end_ts; 93 | } 94 | } 95 | FenceWorkerMessage::EndFrame(frame) => { 96 | let tracker = self.tracker.take().unwrap(); 97 | assert_eq!(Arc::as_ptr(&frame), Weak::as_ptr(&tracker.frame)); 98 | if let Some(begin_ts) = tracker.begin_ts { 99 | frame.mark(1000, MarkType::Begin, begin_ts); 100 | } 101 | if let Some(end_ts) = tracker.end_ts { 102 | frame.mark(1000, MarkType::End, end_ts); 103 | } 104 | frame.set_inv_throughput(1000, tracker.duration); 105 | if let Some(queueing_delay) = tracker.queuing_delay { 106 | frame.set_queueing_delay(800, queueing_delay); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::Mutex; 2 | use std::collections::{BTreeMap, VecDeque}; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | use std::sync::{Arc, Once, Weak}; 5 | use std::time::Duration; 6 | use std::{cmp, thread}; 7 | 8 | use crate::ewma::EwmaEstimator; 9 | use crate::profiler::Profiler; 10 | use crate::time::*; 11 | 12 | #[cfg(all(feature = "dx12", target_os = "windows"))] 13 | mod dx12; 14 | mod entrypoint; 15 | mod ewma; 16 | mod fence_worker; 17 | mod profiler; 18 | mod time; 19 | #[cfg(feature = "vulkan")] 20 | mod vulkan; 21 | 22 | type SectionId = u32; 23 | type Timestamp = u64; 24 | type Interval = u64; 25 | 26 | #[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)] 27 | pub struct FrameId(u64); 28 | 29 | #[repr(C)] 30 | #[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)] 31 | pub enum MarkType { 32 | Begin, 33 | End, 34 | } 35 | 36 | #[derive(Default)] 37 | pub struct Context { 38 | inner: Mutex, 39 | } 40 | 41 | struct ContextInner { 42 | next_frame_id: FrameId, 43 | frames: BTreeMap, 44 | reference_frame: Option, 45 | reference_delay: Option, 46 | bandwidth_estimator: BTreeMap, 47 | 48 | profiler: Profiler, 49 | } 50 | 51 | impl Default for ContextInner { 52 | fn default() -> Self { 53 | ContextInner { 54 | next_frame_id: FrameId(0), 55 | frames: BTreeMap::new(), 56 | reference_frame: None, 57 | reference_delay: None, 58 | bandwidth_estimator: BTreeMap::new(), 59 | profiler: Profiler::new(), 60 | } 61 | } 62 | } 63 | 64 | /// A write handle for frame markers. 65 | pub struct Frame { 66 | context: Arc, 67 | id: FrameId, 68 | } 69 | 70 | struct FrameImpl { 71 | writer: Weak, 72 | predicted_begin: u64, 73 | predicted_error_delta: i64, 74 | marks: BTreeMap<(SectionId, MarkType), Timestamp>, 75 | 76 | // Overrides 77 | inverse_throughput: BTreeMap, 78 | queueing_delay: BTreeMap, 79 | } 80 | 81 | impl ContextInner { 82 | fn alpha(&self) -> f64 { 83 | 0.15 84 | } 85 | 86 | fn beta(&self) -> f64 { 87 | 0.3 88 | } 89 | 90 | fn frames_iter(&self) -> impl DoubleEndedIterator { 91 | self.reference_frame.iter().chain(self.frames.values()) 92 | } 93 | 94 | fn prepare_frame(&mut self, context: Arc) -> (Arc, Timestamp) { 95 | self.update_estimates(); 96 | 97 | let bias = 2_000_000; 98 | let error = if let Some(actual) = self.reference_delay { 99 | self.frames.iter().fold(actual, |acc, (_, frame)| { 100 | (acc + frame.predicted_error_delta).max(0) 101 | }) - bias 102 | } else { 103 | 0 104 | }; 105 | 106 | let clamped_error = error.clamp(-25_000_000, 25_000_000); 107 | 108 | let now = timestamp_now(); 109 | let predicted_duration = self 110 | .bandwidth_estimator 111 | .iter() 112 | .map(|(_, e)| e.get() as u64) 113 | .max() 114 | .unwrap_or(0); 115 | let mut predicted_error_delta = -(self.alpha() * clamped_error as f64) as i64; 116 | let target_frame_time = (predicted_duration as i64 - predicted_error_delta) as u64; 117 | 118 | let last_frame_top = self.frames_iter().next_back().map(|f| f.predicted_begin); 119 | let mut target; 120 | if let Some(last_frame_top) = last_frame_top { 121 | target = last_frame_top + target_frame_time; 122 | if let Some(overdue) = now.checked_sub(last_frame_top + target_frame_time) { 123 | target = now; 124 | predicted_error_delta -= overdue as i64; 125 | } 126 | } else { 127 | target = now; 128 | } 129 | 130 | let id = self.next_frame_id; 131 | self.next_frame_id.0 += 1; 132 | 133 | let handle = Arc::new(Frame { context, id }); 134 | 135 | self.frames.insert( 136 | id, 137 | FrameImpl { 138 | writer: Arc::downgrade(&handle), 139 | predicted_begin: target, 140 | predicted_error_delta, 141 | marks: Default::default(), 142 | inverse_throughput: Default::default(), 143 | queueing_delay: Default::default(), 144 | }, 145 | ); 146 | 147 | static LEAK_WARN: Once = Once::new(); 148 | const LEAK_WARN_THRESHOLD: usize = 16; 149 | if self.frames.len() > 16 { 150 | LEAK_WARN.call_once(|| { 151 | eprintln!("LFX2 WARN: More than {LEAK_WARN_THRESHOLD} frames in flight. Did you forget to call lfx2FrameRelease()?"); 152 | }); 153 | } 154 | 155 | self.profiler.sleep(id, now, target); 156 | 157 | (handle, target) 158 | } 159 | 160 | fn update_estimates(&mut self) { 161 | const MAX_FRAME_TIME: u64 = 50_000_000; 162 | const MAX_LATENCY: u64 = 200_000_000; 163 | 164 | while let Some(first) = self.frames.first_entry() { 165 | if first.get().writer.strong_count() != 0 { 166 | break; 167 | } 168 | 169 | let (frame_id, frame) = first.remove_entry(); 170 | 171 | if let Some(reference_frame) = &self.reference_frame { 172 | let queueing_delay = frame.queueing_delay(reference_frame); 173 | // Should not overflow, but for sanity 174 | let real_latency = frame.end_ts().saturating_sub(frame.begin_ts()); 175 | 176 | self.reference_delay = Some(queueing_delay as i64); 177 | 178 | self.profiler 179 | .latency(frame_id, real_latency, queueing_delay, frame.end_ts()); 180 | 181 | self.profiler.frame_time( 182 | frame_id, 183 | frame.begin_ts() - reference_frame.begin_ts(), 184 | frame.end_ts() - reference_frame.end_ts(), 185 | frame.end_ts(), 186 | ); 187 | } 188 | 189 | for (section_id, duration) in frame.inverse_throughput().into_iter() { 190 | let duration = frame 191 | .inverse_throughput 192 | .get(§ion_id) 193 | .map(|x| *x) 194 | .unwrap_or(duration); 195 | let beta = self.beta(); 196 | self.bandwidth_estimator 197 | .entry(section_id) 198 | .or_insert_with(|| EwmaEstimator::new(beta)) 199 | .update(cmp::min(duration, MAX_FRAME_TIME) as f64); 200 | } 201 | 202 | self.reference_frame = Some(frame); 203 | } 204 | } 205 | } 206 | 207 | impl Frame { 208 | fn mark(&self, section_id: SectionId, mark_type: MarkType, timestamp: Timestamp) { 209 | let mut inner = self.context.inner.lock(); 210 | inner 211 | .frames 212 | .get_mut(&self.id) 213 | .unwrap() 214 | .mark(section_id, mark_type, timestamp); 215 | inner 216 | .profiler 217 | .mark(self.id, section_id, mark_type, timestamp); 218 | } 219 | 220 | fn set_inv_throughput(&self, section_id: SectionId, inv_throughput: Interval) { 221 | let mut inner = self.context.inner.lock(); 222 | inner 223 | .frames 224 | .get_mut(&self.id) 225 | .unwrap() 226 | .set_inv_throughput(section_id, inv_throughput); 227 | } 228 | 229 | fn set_queueing_delay(&self, section_id: SectionId, queueing_delay: Interval) { 230 | let mut inner = self.context.inner.lock(); 231 | inner 232 | .frames 233 | .get_mut(&self.id) 234 | .unwrap() 235 | .set_queueing_delay(section_id, queueing_delay); 236 | } 237 | } 238 | 239 | fn filter_marks_by_type( 240 | marks: &BTreeMap<(SectionId, MarkType), Timestamp>, 241 | mark_type: MarkType, 242 | ) -> Vec<(SectionId, Timestamp)> { 243 | marks 244 | .iter() 245 | .filter_map(|((section_id, mark_type_), timestamp)| { 246 | if *mark_type_ == mark_type { 247 | Some((*section_id, *timestamp)) 248 | } else { 249 | None 250 | } 251 | }) 252 | .collect() 253 | } 254 | 255 | impl FrameImpl { 256 | fn begin_ts(&self) -> Timestamp { 257 | self.marks.first_key_value().map(|x| *x.1).unwrap() 258 | } 259 | 260 | fn end_ts(&self) -> Timestamp { 261 | self.marks.last_key_value().map(|x| *x.1).unwrap() 262 | } 263 | 264 | fn mark(&mut self, section_id: SectionId, mark_type: MarkType, timestamp: Timestamp) { 265 | self.marks.insert((section_id, mark_type), timestamp); 266 | } 267 | 268 | fn set_inv_throughput(&mut self, section_id: SectionId, duration: Interval) { 269 | self.inverse_throughput.insert(section_id, duration); 270 | } 271 | 272 | fn set_queueing_delay(&mut self, section_id: SectionId, queueing_delay: Interval) { 273 | self.queueing_delay.insert(section_id, queueing_delay); 274 | } 275 | 276 | fn queueing_delay(&self, reference: &FrameImpl) -> u64 { 277 | let ends = filter_marks_by_type(&self.marks, MarkType::End); 278 | let last_ends = filter_marks_by_type(&reference.marks, MarkType::End); 279 | let mut delays = Vec::new(); 280 | for (section_id, handoff_time) in ends { 281 | if let Some(&delay) = self.queueing_delay.get(§ion_id) { 282 | delays.push(delay); 283 | continue; 284 | } 285 | let stage_after_idx = 286 | last_ends.partition_point(|&(other_section_id, _)| other_section_id <= section_id); 287 | if let Some(&(_, last_end_time)) = last_ends.get(stage_after_idx) { 288 | delays.push(last_end_time.saturating_sub(handoff_time)); 289 | } 290 | } 291 | delays.into_iter().sum() 292 | } 293 | 294 | fn inverse_throughput(&self) -> BTreeMap { 295 | let begins = filter_marks_by_type(&self.marks, MarkType::Begin); 296 | let ends = filter_marks_by_type(&self.marks, MarkType::End); 297 | ends.into_iter() 298 | .filter_map(|(section_id, timestamp)| { 299 | if let Some(&duration) = self.inverse_throughput.get(§ion_id) { 300 | return Some((section_id, duration)); 301 | } 302 | 303 | let other_timestamp_idx = begins.binary_search_by_key(§ion_id, |&(id, _)| id); 304 | if let Ok(other_timestamp_idx) = other_timestamp_idx { 305 | let (_, other_timestamp) = begins[other_timestamp_idx]; 306 | let duration = timestamp.saturating_sub(other_timestamp); 307 | Some((section_id, duration)) 308 | } else { 309 | None 310 | } 311 | }) 312 | .collect() 313 | } 314 | } 315 | 316 | #[derive(Default)] 317 | pub struct ImplicitContext { 318 | inner: Mutex, 319 | need_reset: AtomicBool, 320 | } 321 | 322 | #[derive(Default)] 323 | struct ImplicitContextInner { 324 | context: Arc, 325 | frame_queue: VecDeque>, 326 | } 327 | 328 | impl ImplicitContext { 329 | fn enqueue(&self) -> (Arc, Timestamp) { 330 | const RESET_FLUSH_TIME: Duration = Duration::from_millis(200); 331 | const RENDER_DESYNC_THRESHOLD: usize = 16; 332 | 333 | let mut inner = if self.need_reset.load(Ordering::SeqCst) { 334 | thread::sleep(RESET_FLUSH_TIME); 335 | let mut inner = self.inner.lock(); 336 | self.need_reset.store(false, Ordering::SeqCst); 337 | inner.frame_queue.clear(); 338 | eprintln!("LFX2: Reset implicit context done"); 339 | inner 340 | } else { 341 | self.inner.lock() 342 | }; 343 | 344 | let mut context = inner.context.inner.lock(); 345 | let (frame, timestamp) = context.prepare_frame(inner.context.clone()); 346 | drop(context); 347 | inner.frame_queue.push_back(frame.clone()); 348 | 349 | if inner.frame_queue.len() > RENDER_DESYNC_THRESHOLD { 350 | eprintln!("LFX2: Resetting implicit context: too many inflight frames"); 351 | self.need_reset.store(true, Ordering::SeqCst); 352 | } 353 | 354 | (frame, timestamp) 355 | } 356 | 357 | fn dequeue(&self, critical: bool) -> Option> { 358 | if self.need_reset.load(Ordering::SeqCst) { 359 | return None; 360 | } 361 | let mut inner = self.inner.lock(); 362 | match inner.frame_queue.pop_front() { 363 | Some(frame) => Some(frame), 364 | None => { 365 | if critical { 366 | eprintln!("LFX2: Resetting implicit context: too many inflight frames"); 367 | self.need_reset.store(true, Ordering::SeqCst); 368 | } 369 | None 370 | } 371 | } 372 | } 373 | 374 | fn reset(&self) { 375 | let _mutex = self.inner.lock(); 376 | eprintln!("LFX2: Resetting implicit context: swapchain recreated"); 377 | self.need_reset.store(true, Ordering::SeqCst); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /core/src/profiler.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{File, OpenOptions}; 2 | use std::io::{BufWriter, Write}; 3 | use std::thread::sleep; 4 | 5 | use chrono::Local; 6 | 7 | use crate::{FrameId, Interval, MarkType, SectionId, Timestamp}; 8 | 9 | pub struct Profiler { 10 | output: BufWriter, 11 | is_first_mark: bool, 12 | } 13 | 14 | impl Profiler { 15 | pub fn new() -> Profiler { 16 | let mut output = BufWriter::new(loop { 17 | let filename = format!("lfx2.{}.json", Local::now().format("%Y.%m.%d-%H.%M.%S")); 18 | let result = OpenOptions::new() 19 | .read(true) 20 | .write(true) 21 | .create_new(true) 22 | .open(&filename); 23 | match result { 24 | Ok(f) => break f, 25 | Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { 26 | sleep(std::time::Duration::from_secs(1)); 27 | } 28 | Err(e) => panic!("Failed to open file {}: {}", filename, e), 29 | } 30 | }); 31 | writeln!(output, "[").unwrap(); 32 | Profiler { 33 | output, 34 | is_first_mark: true, 35 | } 36 | } 37 | 38 | pub fn mark( 39 | &mut self, 40 | frame_id: FrameId, 41 | section_id: SectionId, 42 | mark_type: MarkType, 43 | timestamp: Timestamp, 44 | ) { 45 | let name = frame_id.0; 46 | let tid = section_id; 47 | let ph = match mark_type { 48 | MarkType::Begin => "B", 49 | MarkType::End => "E", 50 | }; 51 | let ts = timestamp / 1000; 52 | let comma = if self.is_first_mark { "" } else { ",\n" }; 53 | self.is_first_mark = false; 54 | let _ = write!( 55 | self.output, 56 | r#"{comma} {{"name": "{name}", "cat": "MARKER", "ph": "{ph}", "pid": 1, "tid": {tid}, "ts": {ts}}}"# 57 | ); 58 | } 59 | 60 | pub fn latency( 61 | &mut self, 62 | frame_id: FrameId, 63 | latency: Interval, 64 | queueing_delay: Interval, 65 | finish_time: Timestamp, 66 | ) { 67 | let ts = finish_time / 1000; 68 | let comma = if self.is_first_mark { "" } else { ",\n" }; 69 | self.is_first_mark = false; 70 | let _ = write!( 71 | self.output, 72 | r#"{comma} {{"name": "Latency", "cat": "LATENCY", "ph": "C", "pid": 1, "tid": "10000", "ts": {ts}, "args": {{"latency": {latency}, "queueing_delay": {queueing_delay}}}}}"# 73 | ); 74 | } 75 | 76 | pub fn frame_time( 77 | &mut self, 78 | frame_id: FrameId, 79 | top_interval: Interval, 80 | bop_interval: Interval, 81 | finish_time: Timestamp, 82 | ) { 83 | let name = frame_id.0; 84 | let ts = finish_time / 1000; 85 | let comma = if self.is_first_mark { "" } else { ",\n" }; 86 | self.is_first_mark = false; 87 | let _ = write!( 88 | self.output, 89 | r#"{comma} {{"name": "Frame Time", "cat": "LATENCY", "ph": "C", "pid": 1, "tid": "10000", "ts": {ts}, "args": {{"top_interval": {top_interval}, "bop_interval": {bop_interval}}}}}"# 90 | ); 91 | } 92 | 93 | pub fn sleep( 94 | &mut self, 95 | frame_id: FrameId, 96 | start_time: Timestamp, 97 | end_time: Timestamp, 98 | ) { 99 | let name = "Sleep"; 100 | let tid = 9999; 101 | let start = start_time / 1000; 102 | let end = end_time / 1000; 103 | let comma = if self.is_first_mark { "" } else { ",\n" }; 104 | self.is_first_mark = false; 105 | let _ = write!( 106 | self.output, 107 | r#"{comma} {{"name": "{name}", "cat": "MARKER", "ph": "B", "pid": 1, "tid": {tid}, "ts": {start}}}, 108 | {{"name": "{name}", "cat": "MARKER", "ph": "E", "pid": 1, "tid": {tid}, "ts": {end}}}"# 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /core/src/time/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(unix, path = "unix.rs")] 2 | #[cfg_attr(windows, path = "windows.rs")] 3 | mod platform; 4 | 5 | pub use platform::*; 6 | 7 | #[cfg(test)] 8 | mod tests { 9 | use super::*; 10 | 11 | #[test] 12 | #[ignore] // Flaky on CI due to VMs, so run it on a local machine. 13 | fn test_sleep_accuracy() { 14 | const THRESHOLD: u64 = 20_000; 15 | const PERCENTILE: f64 = 99.0; 16 | const DURATION: u64 = 100_000; 17 | const ITER: u64 = 1000; 18 | 19 | let below_thresh = (0..ITER) 20 | .filter(|_| { 21 | let begin = timestamp_now(); 22 | sleep_until(begin + DURATION); 23 | let end = timestamp_now(); 24 | assert!(end - begin >= DURATION); 25 | end - begin <= DURATION + THRESHOLD 26 | }) 27 | .count(); 28 | assert!((below_thresh as f64) >= (ITER as f64) * PERCENTILE / 100.0); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/time/unix.rs: -------------------------------------------------------------------------------- 1 | use crate::Timestamp; 2 | use nix::libc::{clock_nanosleep, prctl, PR_SET_TIMERSLACK}; 3 | use nix::sys::time::{TimeSpec, TimeValLike}; 4 | use nix::time::{clock_gettime, ClockId}; 5 | #[cfg(feature = "vulkan")] 6 | use spark::vk; 7 | use std::ptr; 8 | use std::sync::Once; 9 | 10 | #[cfg(feature = "vulkan")] 11 | pub const VULKAN_TIMESTAMP_DOMAIN: vk::TimeDomainEXT = vk::TimeDomainEXT::CLOCK_MONOTONIC; 12 | #[cfg(feature = "vulkan")] 13 | pub fn timestamp_from_vulkan(calibration: u64) -> u64 { 14 | calibration 15 | } 16 | 17 | pub fn timestamp_now() -> Timestamp { 18 | let ts = clock_gettime(ClockId::CLOCK_MONOTONIC).unwrap(); 19 | ts.num_nanoseconds() as _ 20 | } 21 | 22 | pub fn sleep_until(target: Timestamp) { 23 | static SET_TIMERSLACK: Once = Once::new(); 24 | 25 | SET_TIMERSLACK.call_once(|| unsafe { 26 | prctl(PR_SET_TIMERSLACK, 1); 27 | }); 28 | 29 | let ts = TimeSpec::nanoseconds(target as i64); 30 | unsafe { 31 | clock_nanosleep( 32 | ClockId::CLOCK_MONOTONIC.into(), 33 | nix::libc::TIMER_ABSTIME, 34 | ts.as_ref(), 35 | ptr::null_mut(), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/time/windows.rs: -------------------------------------------------------------------------------- 1 | use std::hint; 2 | use std::mem; 3 | use std::num::NonZeroU64; 4 | 5 | use once_cell::sync::Lazy; 6 | #[cfg(feature = "vulkan")] 7 | use spark::vk; 8 | use windows::Win32::Foundation::{CloseHandle, BOOLEAN, HANDLE, NTSTATUS}; 9 | use windows::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress}; 10 | use windows::Win32::System::Performance::{QueryPerformanceCounter, QueryPerformanceFrequency}; 11 | use windows::Win32::System::Threading::{ 12 | CreateWaitableTimerExW, SetWaitableTimer, WaitForSingleObject, 13 | CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS, 14 | }; 15 | use windows::Win32::System::WindowsProgramming::INFINITE; 16 | use windows::{s, w}; 17 | 18 | use crate::Timestamp; 19 | 20 | #[cfg(feature = "vulkan")] 21 | pub const VULKAN_TIMESTAMP_DOMAIN: vk::TimeDomainEXT = vk::TimeDomainEXT::QUERY_PERFORMANCE_COUNTER; 22 | #[cfg(feature = "vulkan")] 23 | pub fn timestamp_from_vulkan(calibration: u64) -> u64 { 24 | timestamp_from_qpc(calibration) 25 | } 26 | 27 | pub fn timestamp_from_qpc(qpc: u64) -> Timestamp { 28 | static QPF: Lazy = Lazy::new(|| { 29 | let mut qpf = 0i64; 30 | unsafe { 31 | QueryPerformanceFrequency(&mut qpf); 32 | } 33 | NonZeroU64::new(qpf as u64).unwrap() 34 | }); 35 | 36 | let denom = 1_000_000_000; 37 | let whole = qpc / QPF.get() * denom; 38 | let part = qpc % QPF.get() * denom / QPF.get(); 39 | (whole + part) as _ 40 | } 41 | 42 | pub fn timestamp_now() -> Timestamp { 43 | let mut qpc = 0i64; 44 | unsafe { 45 | QueryPerformanceCounter(&mut qpc); 46 | } 47 | timestamp_from_qpc(qpc as u64) 48 | } 49 | 50 | struct WaitableTimer(HANDLE); 51 | 52 | impl WaitableTimer { 53 | fn new() -> WaitableTimer { 54 | WaitableTimer( 55 | unsafe { 56 | CreateWaitableTimerExW( 57 | None, 58 | None, 59 | CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, 60 | TIMER_ALL_ACCESS.0, 61 | ) 62 | } 63 | .unwrap(), 64 | ) 65 | } 66 | } 67 | 68 | impl Drop for WaitableTimer { 69 | fn drop(&mut self) { 70 | unsafe { 71 | CloseHandle(self.0); 72 | } 73 | } 74 | } 75 | 76 | thread_local! { 77 | static TIMER: WaitableTimer = WaitableTimer::new(); 78 | } 79 | 80 | static NT_DELAY_EXECUTION: Lazy NTSTATUS>> = 81 | Lazy::new(|| unsafe { 82 | let ntdll = GetModuleHandleW(w!("ntdll.dll")).ok()?; 83 | let wine = GetProcAddress(ntdll, s!("wine_get_version")).is_some(); 84 | if !wine { 85 | return None; 86 | } 87 | let delay_execution = GetProcAddress(ntdll, s!("NtDelayExecution"))?; 88 | Some(mem::transmute(delay_execution)) 89 | }); 90 | 91 | pub fn sleep_until(target: Timestamp) { 92 | const MIN_SPIN_PERIOD: u64 = 500_000; 93 | let mut now = timestamp_now(); 94 | 95 | while now + MIN_SPIN_PERIOD < target { 96 | let sleep_duration = -((target - now - MIN_SPIN_PERIOD) as i64 + 99) / 100; 97 | if let Some(delay_execution) = *NT_DELAY_EXECUTION { 98 | unsafe { 99 | delay_execution(false.into(), &sleep_duration).ok().unwrap(); 100 | } 101 | } else { 102 | TIMER.with(|timer| unsafe { 103 | SetWaitableTimer(timer.0, &sleep_duration, 0, None, None, false) 104 | .ok() 105 | .unwrap(); 106 | WaitForSingleObject(timer.0, INFINITE).ok().unwrap(); 107 | }); 108 | } 109 | now = timestamp_now(); 110 | } 111 | 112 | while now < target { 113 | hint::spin_loop(); 114 | now = timestamp_now(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /core/src/vulkan/entrypoint.rs: -------------------------------------------------------------------------------- 1 | use crate::time::timestamp_now; 2 | use crate::vulkan::{Device, VulkanContext, VulkanSubmitAux}; 3 | use crate::{Frame, MarkType}; 4 | use spark::{vk, Builder}; 5 | use std::sync::Arc; 6 | 7 | #[no_mangle] 8 | pub unsafe extern "C" fn lfx2VulkanContextCreate( 9 | gipa: vk::FnGetInstanceProcAddr, 10 | instance: vk::Instance, 11 | physical_device: vk::PhysicalDevice, 12 | device: vk::Device, 13 | queue_family_index: u32, 14 | ) -> *mut VulkanContext { 15 | let loader = spark::Loader { 16 | fp_create_instance: None, 17 | fp_get_instance_proc_addr: Some(gipa), 18 | fp_enumerate_instance_version: None, 19 | fp_enumerate_instance_layer_properties: None, 20 | fp_enumerate_instance_extension_properties: None, 21 | }; 22 | let stub_instance_create_info = vk::InstanceCreateInfo::builder(); 23 | let mut device_extensions = spark::DeviceExtensions::new(vk::Version::from_raw_parts(1, 3, 0)); 24 | device_extensions.enable_ext_calibrated_timestamps(); 25 | let device_extension_names = device_extensions.to_name_vec(); 26 | let device_extension_names = device_extension_names 27 | .iter() 28 | .map(|s| s.as_ptr()) 29 | .collect::>(); 30 | let stub_device_create_info = 31 | vk::DeviceCreateInfo::builder().pp_enabled_extension_names(&device_extension_names); 32 | let instance = spark::Instance::load(&loader, instance, &stub_instance_create_info).unwrap(); 33 | let device = spark::Device::load( 34 | &instance, 35 | device, 36 | &stub_device_create_info, 37 | vk::Version::from_raw_parts(1, 3, 0), 38 | ) 39 | .unwrap(); 40 | let device = Device::new(instance, physical_device, device, queue_family_index); 41 | let context = VulkanContext::new(device).unwrap(); 42 | Arc::into_raw(context) as _ 43 | } 44 | 45 | #[no_mangle] 46 | pub unsafe extern "C" fn lfx2VulkanContextAddRef(context: *mut VulkanContext) { 47 | Arc::increment_strong_count(context); 48 | } 49 | 50 | #[no_mangle] 51 | pub unsafe extern "C" fn lfx2VulkanContextRelease(context: *mut VulkanContext) { 52 | Arc::decrement_strong_count(context); 53 | } 54 | 55 | #[no_mangle] 56 | pub unsafe extern "C" fn lfx2VulkanContextBeforeSubmit( 57 | context: *mut VulkanContext, 58 | ) -> VulkanSubmitAux { 59 | (*context).inner.lock().submit().unwrap() 60 | } 61 | 62 | #[no_mangle] 63 | pub unsafe extern "C" fn lfx2VulkanContextBeginFrame( 64 | context: *mut VulkanContext, 65 | frame: *mut Frame, 66 | ) { 67 | let frame = Arc::from_raw(frame); 68 | frame.mark(800, MarkType::Begin, timestamp_now()); 69 | (*context).inner.lock().begin(&frame); 70 | let _ = Arc::into_raw(frame); 71 | } 72 | 73 | #[no_mangle] 74 | pub unsafe extern "C" fn lfx2VulkanContextEndFrame(context: *mut VulkanContext, frame: *mut Frame) { 75 | let frame = Arc::from_raw(frame); 76 | (*context).inner.lock().end(&frame); 77 | frame.mark(800, MarkType::End, timestamp_now()); 78 | let _ = Arc::into_raw(frame); 79 | } 80 | -------------------------------------------------------------------------------- /core/src/vulkan/mod.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::sync::Arc; 3 | 4 | use parking_lot::Mutex; 5 | use spark::vk::{CommandBufferUsageFlags, QueryResultFlags}; 6 | use spark::{vk, Builder}; 7 | 8 | use crate::fence_worker::{FenceThread, FenceWorkerMessage}; 9 | use crate::time::{timestamp_from_vulkan, timestamp_now, VULKAN_TIMESTAMP_DOMAIN}; 10 | use crate::{Frame, Timestamp}; 11 | 12 | mod entrypoint; 13 | 14 | type VkResult = Result; 15 | 16 | struct Device { 17 | handle: spark::Device, 18 | limits: vk::PhysicalDeviceLimits, 19 | queue_family_index: u32, 20 | queue_family_properties: vk::QueueFamilyProperties, 21 | } 22 | 23 | impl Device { 24 | fn new( 25 | instance: spark::Instance, 26 | phys_device: vk::PhysicalDevice, 27 | device: spark::Device, 28 | queue_family_index: u32, 29 | ) -> Arc { 30 | unsafe { 31 | let limits = instance.get_physical_device_properties(phys_device).limits; 32 | let queue_family_properties = instance 33 | .get_physical_device_queue_family_properties_to_vec(phys_device) 34 | [queue_family_index as usize]; 35 | Arc::new(Device { 36 | handle: device, 37 | limits, 38 | queue_family_index, 39 | queue_family_properties, 40 | }) 41 | } 42 | } 43 | } 44 | 45 | struct QueryPool { 46 | device: Arc, 47 | handle: vk::QueryPool, 48 | } 49 | 50 | impl QueryPool { 51 | fn new(device: Arc, query_type: vk::QueryType, count: u32) -> VkResult> { 52 | let handle = unsafe { 53 | device.handle.create_query_pool( 54 | &vk::QueryPoolCreateInfo::builder() 55 | .query_type(query_type) 56 | .query_count(count), 57 | None, 58 | )? 59 | }; 60 | Ok(Arc::new(Self { device, handle })) 61 | } 62 | } 63 | 64 | impl Drop for QueryPool { 65 | fn drop(&mut self) { 66 | unsafe { 67 | self.device 68 | .handle 69 | .destroy_query_pool(Some(self.handle), None); 70 | } 71 | } 72 | } 73 | 74 | struct CommandBuffer { 75 | device: Arc, 76 | pool: vk::CommandPool, 77 | handle: vk::CommandBuffer, 78 | } 79 | 80 | impl CommandBuffer { 81 | // NOTE: `pool` must outlive the CommandBuffer 82 | // TODO: Create a safe wrapper for CommandPool 83 | fn new(device: Arc, pool: vk::CommandPool) -> VkResult { 84 | let handle = unsafe { 85 | device.handle.allocate_command_buffers_to_vec( 86 | &vk::CommandBufferAllocateInfo::builder() 87 | .command_pool(pool) 88 | .command_buffer_count(1), 89 | ) 90 | }?[0]; 91 | Ok(Self { 92 | device, 93 | pool, 94 | handle, 95 | }) 96 | } 97 | 98 | fn reset(&mut self) -> VkResult<()> { 99 | unsafe { 100 | self.device 101 | .handle 102 | .reset_command_buffer(self.handle, vk::CommandBufferResetFlags::empty()) 103 | } 104 | } 105 | } 106 | 107 | impl Drop for CommandBuffer { 108 | fn drop(&mut self) { 109 | unsafe { 110 | self.device 111 | .handle 112 | .free_command_buffers(self.pool, &[self.handle]); 113 | } 114 | } 115 | } 116 | 117 | pub struct VulkanContext { 118 | inner: Mutex, 119 | } 120 | 121 | struct VulkanContextInner { 122 | device: Arc, 123 | command_pool: vk::CommandPool, 124 | command_buffer: Vec, 125 | query_pool: Vec<(Arc, u32)>, 126 | 127 | fence_thread: Option>, 128 | 129 | sem: vk::Semaphore, 130 | seq: u64, 131 | } 132 | 133 | #[repr(C)] 134 | pub struct VulkanSubmitAux { 135 | submit_before: vk::CommandBuffer, 136 | submit_after: vk::CommandBuffer, 137 | signal_sem: vk::Semaphore, 138 | signal_sem_value: u64, 139 | } 140 | 141 | impl VulkanContext { 142 | fn new(device: Arc) -> VkResult> { 143 | let ret = Arc::new(VulkanContext { 144 | inner: Mutex::new(VulkanContextInner::new(device)?), 145 | }); 146 | let weak = Arc::downgrade(&ret); 147 | ret.inner.lock().fence_thread = 148 | Some(FenceThread::new(move |submission: VulkanSubmission| { 149 | let ctx = weak.upgrade().unwrap(); 150 | submission.complete(&ctx).unwrap() 151 | })); 152 | Ok(ret) 153 | } 154 | } 155 | 156 | impl VulkanContextInner { 157 | fn new(device: Arc) -> VkResult { 158 | let command_pool = unsafe { 159 | device.handle.create_command_pool( 160 | &vk::CommandPoolCreateInfo::builder() 161 | .queue_family_index(device.queue_family_index) 162 | .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER), 163 | None, 164 | ) 165 | } 166 | .unwrap(); 167 | 168 | let sem = unsafe { 169 | device.handle.create_semaphore( 170 | &vk::SemaphoreCreateInfo::builder().insert_next( 171 | &mut vk::SemaphoreTypeCreateInfo::builder() 172 | .semaphore_type(vk::SemaphoreType::TIMELINE), 173 | ), 174 | None, 175 | )? 176 | }; 177 | 178 | Ok(Self { 179 | device, 180 | command_pool, 181 | command_buffer: Vec::new(), 182 | query_pool: Vec::new(), 183 | fence_thread: None, 184 | sem, 185 | seq: 1, 186 | }) 187 | } 188 | } 189 | 190 | impl Drop for VulkanContextInner { 191 | fn drop(&mut self) { 192 | // Drop existing references to command pool first 193 | self.command_buffer.clear(); 194 | // Optional, but for sanity 195 | self.query_pool.clear(); 196 | 197 | unsafe { 198 | self.device.handle.destroy_semaphore(Some(self.sem), None); 199 | self.device 200 | .handle 201 | .destroy_command_pool(Some(self.command_pool), None); 202 | } 203 | } 204 | } 205 | 206 | impl VulkanContextInner { 207 | fn get_query_pool(&mut self) -> VkResult<(Arc, u32)> { 208 | let (pool, idx) = if let Some((pool, idx)) = self.query_pool.pop() { 209 | (pool, idx) 210 | } else { 211 | let count = 16; 212 | let pool = QueryPool::new(self.device.clone(), vk::QueryType::TIMESTAMP, count)?; 213 | self.query_pool 214 | .extend((0..count).map(|i| (pool.clone(), i))); 215 | self.query_pool.pop().unwrap() 216 | }; 217 | unsafe { 218 | self.device.handle.reset_query_pool(pool.handle, idx, 1); 219 | } 220 | Ok((pool, idx)) 221 | } 222 | 223 | fn get_command_buffer(&mut self) -> VkResult { 224 | if let Some(cmd) = self.command_buffer.pop() { 225 | Ok(cmd) 226 | } else { 227 | CommandBuffer::new(self.device.clone(), self.command_pool) 228 | } 229 | } 230 | 231 | fn begin(&mut self, frame: &Arc) { 232 | self.fence_thread 233 | .as_mut() 234 | .unwrap() 235 | .send(FenceWorkerMessage::BeginFrame(Arc::downgrade(frame))); 236 | } 237 | 238 | fn end(&mut self, frame: &Arc) { 239 | self.fence_thread 240 | .as_mut() 241 | .unwrap() 242 | .send(FenceWorkerMessage::EndFrame(frame.clone())); 243 | } 244 | 245 | fn submit(&mut self) -> VkResult { 246 | let queries = (0..2) 247 | .map(|_| self.get_query_pool()) 248 | .collect::>>()?; 249 | let command_buffers = (0..2) 250 | .map(|_| self.get_command_buffer()) 251 | .collect::>>()?; 252 | for i in 0..2 { 253 | unsafe { 254 | self.device.handle.begin_command_buffer( 255 | command_buffers[i].handle, 256 | &vk::CommandBufferBeginInfo::builder() 257 | .flags(CommandBufferUsageFlags::ONE_TIME_SUBMIT), 258 | )?; 259 | self.device.handle.cmd_write_timestamp2( 260 | command_buffers[i].handle, 261 | vk::PipelineStageFlags2::ALL_COMMANDS, 262 | queries[i].0.handle, 263 | queries[i].1, 264 | ); 265 | self.device 266 | .handle 267 | .end_command_buffer(command_buffers[i].handle)?; 268 | } 269 | } 270 | let seq = self.seq; 271 | self.seq += 1; 272 | 273 | let ret = VulkanSubmitAux { 274 | submit_before: command_buffers[0].handle, 275 | submit_after: command_buffers[1].handle, 276 | signal_sem: self.sem, 277 | signal_sem_value: seq, 278 | }; 279 | 280 | self.fence_thread 281 | .as_mut() 282 | .unwrap() 283 | .send(FenceWorkerMessage::Wait(VulkanSubmission { 284 | submission_ts: timestamp_now(), 285 | queries: queries.try_into().map_err(|_| ()).unwrap(), 286 | command_buffers: command_buffers.try_into().map_err(|_| ()).unwrap(), 287 | seq, 288 | })); 289 | 290 | Ok(ret) 291 | } 292 | } 293 | 294 | struct VulkanSubmission { 295 | queries: [(Arc, u32); 2], 296 | command_buffers: [CommandBuffer; 2], 297 | seq: u64, 298 | 299 | submission_ts: Timestamp, 300 | } 301 | 302 | impl VulkanSubmission { 303 | fn complete(self, context: &VulkanContext) -> VkResult<(Timestamp, Timestamp, Timestamp)> { 304 | let device; 305 | let sem; 306 | { 307 | let lock = context.inner.lock(); 308 | device = lock.device.clone(); 309 | sem = lock.sem; 310 | } 311 | unsafe { 312 | device.handle.wait_semaphores( 313 | &vk::SemaphoreWaitInfo::builder().p_semaphores(&[sem], &[self.seq]), 314 | u64::MAX, 315 | )?; 316 | } 317 | 318 | let mut calibration = [0u64; 2]; 319 | let mut deviation = 0u64; 320 | let timestamp_info = [ 321 | *vk::CalibratedTimestampInfoEXT::builder().time_domain(vk::TimeDomainEXT::DEVICE), 322 | *vk::CalibratedTimestampInfoEXT::builder().time_domain(VULKAN_TIMESTAMP_DOMAIN), 323 | ]; 324 | unsafe { 325 | device.handle.get_calibrated_timestamps_ext( 326 | ×tamp_info, 327 | calibration.as_mut_ptr(), 328 | &mut deviation, 329 | )?; 330 | } 331 | let process_timestamp = |(pool, index): &(Arc, u32)| -> VkResult { 332 | let mut gpu_ts = [0u64; 1]; 333 | unsafe { 334 | device.handle.get_query_pool_results( 335 | pool.handle, 336 | *index, 337 | 1, 338 | &mut gpu_ts, 339 | mem::size_of::() as _, 340 | QueryResultFlags::N64 | QueryResultFlags::WAIT, 341 | )?; 342 | } 343 | let gpu_calibration = calibration[0]; 344 | let cpu_calibration = timestamp_from_vulkan(calibration[1]); 345 | let valid_shift = 64 - device.queue_family_properties.timestamp_valid_bits; 346 | let gpu_delta = (gpu_ts[0] as i64 - gpu_calibration as i64) 347 | .wrapping_shl(valid_shift) 348 | .wrapping_shr(valid_shift); 349 | let calibrated = cpu_calibration as i64 350 | + (gpu_delta as f64 * device.limits.timestamp_period as f64) as i64; 351 | Ok(calibrated as u64) 352 | }; 353 | let begin_ts = process_timestamp(&self.queries[0])?; 354 | let end_ts = process_timestamp(&self.queries[1])?; 355 | { 356 | let mut lock = context.inner.lock(); 357 | for query in self.queries { 358 | lock.query_pool.push(query); 359 | } 360 | for mut buf in self.command_buffers { 361 | buf.reset()?; 362 | lock.command_buffer.push(buf); 363 | } 364 | } 365 | Ok((self.submission_ts, begin_ts, end_ts)) 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /.vitepress/cache 2 | /.vitepress/dist -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | export default defineConfig({ 4 | title: 'LatencyFleX 2', 5 | description: '', 6 | themeConfig: { 7 | editLink: { 8 | pattern: 'https://github.com/ishitatsuyuki/latencyflex2/edit/master/docs/:path', 9 | text: 'Edit this page on GitHub', 10 | }, 11 | nav: [ 12 | { text: 'For Players', link: '/shim/building', activeMatch: '/shim/' }, 13 | ], 14 | sidebar: { 15 | '/shim/': [ 16 | { 17 | text: 'Setup', 18 | items: [ 19 | { text: 'Building', link: '/shim/building' }, 20 | { text: 'Installation', link: '/shim/installing' }, 21 | ], 22 | }, 23 | ], 24 | }, 25 | socialLinks: [ 26 | { icon: 'github', link: 'https://github.com/ishitatsuyuki/latencyflex2' }, 27 | ], 28 | }, 29 | markdown: { 30 | theme: { 31 | light: 'github-light', 32 | dark: 'github-dark', 33 | }, 34 | }, 35 | }) -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-layout-max-width: 1600px; 3 | } 4 | 5 | .VPDoc.has-aside .content-container { 6 | max-width: 1024px !important; 7 | } 8 | 9 | html:not(.dark) { 10 | --vp-c-text-dark-3: #ccc; 11 | --vp-code-block-bg: rgba(238, 238, 238, 0.5); 12 | --vp-code-line-highlight-color: rgba(238, 238, 238, 1); 13 | } 14 | 15 | input { 16 | border: 1px solid var(--vp-c-divider); 17 | border-radius: 4px; 18 | padding: 1px 4px; 19 | } -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './custom.css' 3 | 4 | export default DefaultTheme -------------------------------------------------------------------------------- /docs/ea.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Early Access Disclaimer 3 | editLink: true 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | LatencyFleX 2 is currently in the alpha stage. Keep in mind that: 9 | - Things might be broken, and please report game compatibility issues. 10 | - Internal APIs changes frequently. When updating builds, do it for all components at once. 11 | - The public API is subject to change and intentionally undocumented. If you're a game developer, please wait until a stable release of LFX 2 happens. 12 | 13 | During alpha, debug and profiling logging is always enabled. Around 1GB of data is written per hour of gameplay session. Using a filesystem with transparent compression can reduce the amount of I/O. 14 | 15 | Finally, expect bugs since this is alpha stage software. I will not be responsible for any damages, including but not limited to broken setups or corrupted save files. Please follow backup best practices and limit the damage in case something goes wrong. 16 | 17 | With that in mind, proceed to [Building](./shim/building.md). -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: LatencyFleX 2 3 | --- 4 | 5 | # {{ $frontmatter.title }} 6 | 7 | LatencyFleX 2 is currently under heavy development. 8 | 9 | See [documentation](./ea.md) for early access disclaimer and installation instructions. -------------------------------------------------------------------------------- /docs/public/files/EnableSignatureOverride.reg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishitatsuyuki/latencyflex2/d8e42feacaeb0c6073a0dafbef939b66eb6605ec/docs/public/files/EnableSignatureOverride.reg -------------------------------------------------------------------------------- /docs/shim/building.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building 3 | editLink: true 4 | --- 5 | 6 | 11 | 12 | 13 | # {{ $frontmatter.title }} 14 | 15 | Version to build: 16 | 17 | ## Prerequisites 18 | 19 | Install [rustup](https://rustup.rs/) if you haven't already. 20 | 21 | Then, install the MinGW target for Rust: 22 | 23 | ```bash 24 | rustup target add x86_64-pc-windows-gnu 25 | ``` 26 | 27 | ## Compiling the core module 28 | 29 | Clone the LatencyFleX 2 repo: 30 | 31 | ```bash-vue 32 | git clone https://github.com/ishitatsuyuki/latencyflex2.git -b {{version}} 33 | cd latencyflex2 34 | ``` 35 | 36 | Build the module: 37 | 38 | ```bash 39 | cd core 40 | cargo build --release --target x86_64-pc-windows-gnu 41 | ``` 42 | 43 | The module will be available at `target/x86_64-pc-windows-gnu/release/latencyflex2_rust.dll`. 44 | 45 | ## Compiling the DXVK fork 46 | 47 | Clone the fork and checkout the `lfx2-{{version}}` tag: 48 | 49 | ```bash-vue 50 | git clone --recursive https://github.com/ishitatsuyuki/dxvk.git -b lfx2-{{version}} 51 | ``` 52 | 53 | Then follow the upstream [build instructions](https://github.com/doitsujin/dxvk#build-instructions). 54 | 55 | ## Compiling the DXVK-NVAPI fork 56 | 57 | Clone the fork and checkout the `lfx2-{{version}}` tag: 58 | 59 | ```bash-vue 60 | git clone --recursive https://github.com/ishitatsuyuki/dxvk-nvapi.git -b lfx2-{{version}} 61 | ``` 62 | 63 | Then follow the upstream [build instructions](https://github.com/jp7677/dxvk-nvapi#how-to-build). 64 | 65 | ## Compiling the VKD3D-Proton fork 66 | 67 | Clone the fork and checkout the `lfx2-{{version}}` tag: 68 | 69 | ```bash-vue 70 | git clone --recursive https://github.com/ishitatsuyuki/vkd3d-proton.git -b lfx2-{{version}} 71 | ``` 72 | 73 | Then follow the upstream [build instructions](https://github.com/HansKristian-Work/vkd3d-proton#building-vkd3d-proton). 74 | -------------------------------------------------------------------------------- /docs/shim/installing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | editLink: true 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | ## Proton 9 | 10 | ### Prerequisite 11 | 12 | Your Proton installation should be new enough to contain a [fix](https://github.com/ValveSoftware/wine/pull/171) for GPU timestamps. If you're using one of the Proton distributions below, the requirements are: 13 | 14 | - **Proton Experimental (Bleeding Edge)**: 7.0-32084-20221229 or later 15 | - **Proton-GE**: GE-Proton7-44 or later 16 | 17 | ### Overview 18 | 19 | Due to Proton conventions, there are two kind of installation steps: 20 | - Done once per **prefix**: LatencyFleX 2 Core Module 21 | - Done once per **Proton version**: DXVK, DXVK-NVAPI and VKD3D-Proton 22 | 23 | ### Per-prefix setup 24 | 25 | For the following section, set `COMPATDATA` to the path to the app prefix. 26 | 27 | This can be determined from the app's Steam AppID, like: 28 | 29 | ```bash 30 | APPID=1234567 31 | COMPATDATA=~"/.steam/steam/steamapps/compatdata/$APPID" 32 | ``` 33 | 34 | #### Installing the core module 35 | 36 | Copy the just built core module into the `system32` folder under your prefix. 37 | 38 | ```bash 39 | cp target/x86_64-pc-windows-gnu/release/latencyflex2_rust.dll "$COMPATDATA/pfx/drive_c/windows/system32/" 40 | ``` 41 | 42 | ### Per Proton-installation setup 43 | 44 | For the following section, set `PROTON_PATH` to the path to Proton installation, like: 45 | 46 | ```bash 47 | PROTON_PATH=~/.steam/steam/steamapps/common/"Proton - Experimental" 48 | ``` 49 | 50 | #### Installing the DXVK fork 51 | 52 | Overwrite your Proton Experimental installation's DXVK dlls with the just built DLLs. 53 | 54 | ```bash 55 | cp x64/*.dll "$PROTON_PATH/files/lib64/wine/dxvk/" 56 | ``` 57 | 58 | #### Installing the DXVK-NVAPI fork 59 | 60 | Overwrite your Proton Experimental installation's DXVK-NVAPI dlls with the just built DLLs. 61 | 62 | ```bash 63 | cp x64/nvapi64.dll "$PROTON_PATH/files/lib64/wine/nvapi/" 64 | ``` 65 | 66 | #### Installing the VKD3D-Proton fork 67 | 68 | Overwrite your Proton Experimental installation's VKD3D-Proton dlls with the just built DLLs. 69 | 70 | ```bash 71 | cp x64/*.dll "$PROTON_PATH/files/lib64/wine/vkd3d-proton/" 72 | ``` 73 | 74 | Now proceed on to [Environment Variables](#environment-variables) and [Configuration Files](#configuration-files). 75 | 76 | ## Lutris 77 | 78 | ### Prerequisite 79 | 80 | - Wine upstream: 7.0 or later 81 | - Wine-GE: GE-Proton7-36 or later 82 | 83 | For trees based on Proton branches, it is necessary that the [fix](https://github.com/ValveSoftware/wine/pull/171) for GPU timestamps is included. 84 | 85 | ### Installing the core module 86 | 87 | Copy the just built core module into the `system32` folder under your prefix. 88 | 89 | ```bash 90 | cp target/x86_64-pc-windows-gnu/release/latencyflex2_rust.dll ~/Games//drive_c/windows/system32/ 91 | ``` 92 | 93 | ### Installing the DXVK fork 94 | 95 | Create a new DXVK runtime for Lutris with the just built artifacts. 96 | 97 | ```bash 98 | mkdir -p ~/.local/share/lutris/runtime/dxvk/lfx2/ 99 | cp -r x64 ~/.local/share/lutris/runtime/dxvk/lfx2/ 100 | ``` 101 | 102 | Then **Right Click** the game, go to **Configure** → **Runner Options** → **DXVK version** and manually type in "lfx2". 103 | 104 | ::: info 105 | 106 | If you need the 32-bit version of DXVK, please copy it from another upstream build of DXVK. The DXVK fork will crash on startup if LFX2 cannot be loaded, which is the case for 32-bit since we don't install 32-bit LFX2 modules. 107 | 108 | ::: 109 | 110 | ### Installing the DXVK-NVAPI fork 111 | 112 | Create a new DXVK-NVAPI runtime for Lutris with the just built artifacts. 113 | 114 | ```bash 115 | mkdir -p ~/.local/share/lutris/runtime/dxvk-nvapi/lfx2/ 116 | cp -r x64 ~/.local/share/lutris/runtime/dxvk-nvapi/lfx2/ 117 | ``` 118 | 119 | Then **Right Click** the game, go to **Configure** → **Runner Options** → **DXVK-NVAPI version** and manually type in "lfx2". 120 | 121 | ### Installing the VKD3D-Proton fork 122 | 123 | Create a new VKD3D-Proton runtime for Lutris with the just built artifacts. 124 | 125 | ```bash 126 | mkdir -p ~/.local/share/lutris/runtime/vkd3d/lfx2/ 127 | cp -r x64 ~/.local/share/lutris/runtime/vkd3d/lfx2/ 128 | ``` 129 | 130 | Then **Right Click** the game, go to **Configure** → **Runner Options** → **VKD3D version** and manually type in "lfx2". 131 | 132 | Now proceed on to [Environment Variables](#environment-variables) and [Configuration Files](#configuration-files). 133 | 134 | ## Environment Variables 135 | 136 | To configure environment variables, using `KEY=value` as an example: 137 | - Steam/Proton: Set `KEY=value %command%` as the game's **launch command line**. 138 | - Lutris: **Right Click** the game, then set in **Configure** → **System Options** → **Environment Variables**. 139 | 140 | ### Required 141 | 142 | - `PROTON_ENABLE_NVAPI=1` (Proton only): Use this to enable DXVK-NVAPI. 143 | - `DXVK_ENABLE_NVAPI=1` (non-Proton only): Set this to disable DXVK's nvapiHack. 144 | - `DXVK_NVAPI_USE_LATENCY_MARKERS=0`: Set to use no-latency-markers mode (see [Enabling or disabling explicit latency markers](#enabling-or-disabling-explicit-latency-markers)) 145 | 146 | ### Required (Non-NVIDIA GPUs only) 147 | 148 | - `DXVK_NVAPI_DRIVER_VERSION=51215`: Override the driver version as one that has Reflex support. 149 | - `DXVK_NVAPI_ALLOW_OTHER_DRIVERS=1`: Enable NVAPI usage with non-NVIDIA GPUs. 150 | 151 | ### Diagnostics 152 | 153 | - `DXVK_NVAPI_LOG_LEVEL=info`: Set this to enable DXVK-NVAPI logging. 154 | 155 | ## Additional Setup (Non-NVIDIA GPUs only) 156 | 157 | ### dxvk.conf 158 | 159 | Put `dxgi.customVendorId = 10de` in `dxvk.conf` to allow NVAPI usage with non-NVIDIA GPUs. 160 | 161 | ### `nvngx.dll` Workaround 162 | 163 | Some games with DLSS support will hang on launch if NVAPI is spoofed without an NVIDIA driver. 164 | This is caused by the DLSS SDK trying to load `nvngx.dll` and getting stuck in a loop if it does not succeed. 165 | 166 | The following step installs an empty `nvngx.dll` that pleases the DLSS SDK: 167 | 168 | 1. Create an empty `nvngx.dll` with the following command: 169 | ```sh 170 | x86_64-w64-mingw32-cc -shared -static-libgcc -o nvngx.dll /dev/null 171 | ``` 172 | 2. Copy `nvngx.dll` to `$COMPATDATA/pfx/drive_c/windows/system32/`. 173 | 3. Import [EnableSignatureOverride.reg](/files/EnableSignatureOverride.reg). If you have `protontricks` installed, this can be done with: 174 | ```sh 175 | protontricks -c "regedit /path/to/EnableSignatureOverride.reg" $APPID 176 | ``` 177 | 178 | ## Enabling or disabling explicit latency markers 179 | 180 | Before LFX2 can work with the game, you need to determine whether the game uses explicit latency markers or not. 181 | 182 | Configure LFX2 per the steps above, and include `DXVK_NVAPI_LOG_LEVEL=info` in the environment. Now launch the game, and go to the settings to enable Reflex. 183 | 184 | If Reflex was successfully enabled and logging is also working, you should see something like below in the log: 185 | 186 | ``` 187 | NvAPI_D3D_SetSleepMode (Enabled/0us): OK 188 | ``` 189 | 190 | If you don't see this, the configuration might be incorrect. 191 | 192 | Next, check whether the following lines exist in the log: 193 | 194 | ``` 195 | NvAPI_D3D_SetLatencyMarker: OK 196 | ``` 197 | 198 | - If the line appears, the game supports latency markers. You do not need to do any additional configuration. 199 | - If the line doesn't appear, the game does not support latency markers. Set `DXVK_NVAPI_USE_LATENCY_MARKERS=0` in the environment and re-launch the game. 200 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "latencyflex2", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vitepress dev docs", 9 | "build": "vitepress build docs", 10 | "preview": "vitepress preview docs" 11 | }, 12 | "keywords": [], 13 | "author": "Tatsuyuki Ishi ", 14 | "license": "Apache-2.0", 15 | "devDependencies": { 16 | "vitepress": "^1.0.0-rc.22", 17 | "vue": "^3.3.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | devDependencies: 8 | vitepress: 9 | specifier: ^1.0.0-rc.22 10 | version: 1.0.0-rc.22(@algolia/client-search@4.20.0)(search-insights@2.9.0) 11 | vue: 12 | specifier: ^3.3.4 13 | version: 3.3.4 14 | 15 | packages: 16 | 17 | /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0): 18 | resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==} 19 | dependencies: 20 | '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0) 21 | '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0) 22 | transitivePeerDependencies: 23 | - '@algolia/client-search' 24 | - algoliasearch 25 | - search-insights 26 | dev: true 27 | 28 | /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0): 29 | resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==} 30 | peerDependencies: 31 | search-insights: '>= 1 < 3' 32 | dependencies: 33 | '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0) 34 | search-insights: 2.9.0 35 | transitivePeerDependencies: 36 | - '@algolia/client-search' 37 | - algoliasearch 38 | dev: true 39 | 40 | /@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0): 41 | resolution: {integrity: sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==} 42 | peerDependencies: 43 | '@algolia/client-search': '>= 4.9.1 < 6' 44 | algoliasearch: '>= 4.9.1 < 6' 45 | dependencies: 46 | '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0) 47 | '@algolia/client-search': 4.20.0 48 | algoliasearch: 4.20.0 49 | dev: true 50 | 51 | /@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0): 52 | resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==} 53 | peerDependencies: 54 | '@algolia/client-search': '>= 4.9.1 < 6' 55 | algoliasearch: '>= 4.9.1 < 6' 56 | dependencies: 57 | '@algolia/client-search': 4.20.0 58 | algoliasearch: 4.20.0 59 | dev: true 60 | 61 | /@algolia/cache-browser-local-storage@4.20.0: 62 | resolution: {integrity: sha512-uujahcBt4DxduBTvYdwO3sBfHuJvJokiC3BP1+O70fglmE1ShkH8lpXqZBac1rrU3FnNYSUs4pL9lBdTKeRPOQ==} 63 | dependencies: 64 | '@algolia/cache-common': 4.20.0 65 | dev: true 66 | 67 | /@algolia/cache-common@4.20.0: 68 | resolution: {integrity: sha512-vCfxauaZutL3NImzB2G9LjLt36vKAckc6DhMp05An14kVo8F1Yofb6SIl6U3SaEz8pG2QOB9ptwM5c+zGevwIQ==} 69 | dev: true 70 | 71 | /@algolia/cache-in-memory@4.20.0: 72 | resolution: {integrity: sha512-Wm9ak/IaacAZXS4mB3+qF/KCoVSBV6aLgIGFEtQtJwjv64g4ePMapORGmCyulCFwfePaRAtcaTbMcJF+voc/bg==} 73 | dependencies: 74 | '@algolia/cache-common': 4.20.0 75 | dev: true 76 | 77 | /@algolia/client-account@4.20.0: 78 | resolution: {integrity: sha512-GGToLQvrwo7am4zVkZTnKa72pheQeez/16sURDWm7Seyz+HUxKi3BM6fthVVPUEBhtJ0reyVtuK9ArmnaKl10Q==} 79 | dependencies: 80 | '@algolia/client-common': 4.20.0 81 | '@algolia/client-search': 4.20.0 82 | '@algolia/transporter': 4.20.0 83 | dev: true 84 | 85 | /@algolia/client-analytics@4.20.0: 86 | resolution: {integrity: sha512-EIr+PdFMOallRdBTHHdKI3CstslgLORQG7844Mq84ib5oVFRVASuuPmG4bXBgiDbcsMLUeOC6zRVJhv1KWI0ug==} 87 | dependencies: 88 | '@algolia/client-common': 4.20.0 89 | '@algolia/client-search': 4.20.0 90 | '@algolia/requester-common': 4.20.0 91 | '@algolia/transporter': 4.20.0 92 | dev: true 93 | 94 | /@algolia/client-common@4.20.0: 95 | resolution: {integrity: sha512-P3WgMdEss915p+knMMSd/fwiHRHKvDu4DYRrCRaBrsfFw7EQHon+EbRSm4QisS9NYdxbS04kcvNoavVGthyfqQ==} 96 | dependencies: 97 | '@algolia/requester-common': 4.20.0 98 | '@algolia/transporter': 4.20.0 99 | dev: true 100 | 101 | /@algolia/client-personalization@4.20.0: 102 | resolution: {integrity: sha512-N9+zx0tWOQsLc3K4PVRDV8GUeOLAY0i445En79Pr3zWB+m67V+n/8w4Kw1C5LlbHDDJcyhMMIlqezh6BEk7xAQ==} 103 | dependencies: 104 | '@algolia/client-common': 4.20.0 105 | '@algolia/requester-common': 4.20.0 106 | '@algolia/transporter': 4.20.0 107 | dev: true 108 | 109 | /@algolia/client-search@4.20.0: 110 | resolution: {integrity: sha512-zgwqnMvhWLdpzKTpd3sGmMlr4c+iS7eyyLGiaO51zDZWGMkpgoNVmltkzdBwxOVXz0RsFMznIxB9zuarUv4TZg==} 111 | dependencies: 112 | '@algolia/client-common': 4.20.0 113 | '@algolia/requester-common': 4.20.0 114 | '@algolia/transporter': 4.20.0 115 | dev: true 116 | 117 | /@algolia/logger-common@4.20.0: 118 | resolution: {integrity: sha512-xouigCMB5WJYEwvoWW5XDv7Z9f0A8VoXJc3VKwlHJw/je+3p2RcDXfksLI4G4lIVncFUYMZx30tP/rsdlvvzHQ==} 119 | dev: true 120 | 121 | /@algolia/logger-console@4.20.0: 122 | resolution: {integrity: sha512-THlIGG1g/FS63z0StQqDhT6bprUczBI8wnLT3JWvfAQDZX5P6fCg7dG+pIrUBpDIHGszgkqYEqECaKKsdNKOUA==} 123 | dependencies: 124 | '@algolia/logger-common': 4.20.0 125 | dev: true 126 | 127 | /@algolia/requester-browser-xhr@4.20.0: 128 | resolution: {integrity: sha512-HbzoSjcjuUmYOkcHECkVTwAelmvTlgs48N6Owt4FnTOQdwn0b8pdht9eMgishvk8+F8bal354nhx/xOoTfwiAw==} 129 | dependencies: 130 | '@algolia/requester-common': 4.20.0 131 | dev: true 132 | 133 | /@algolia/requester-common@4.20.0: 134 | resolution: {integrity: sha512-9h6ye6RY/BkfmeJp7Z8gyyeMrmmWsMOCRBXQDs4mZKKsyVlfIVICpcSibbeYcuUdurLhIlrOUkH3rQEgZzonng==} 135 | dev: true 136 | 137 | /@algolia/requester-node-http@4.20.0: 138 | resolution: {integrity: sha512-ocJ66L60ABSSTRFnCHIEZpNHv6qTxsBwJEPfYaSBsLQodm0F9ptvalFkHMpvj5DfE22oZrcrLbOYM2bdPJRHng==} 139 | dependencies: 140 | '@algolia/requester-common': 4.20.0 141 | dev: true 142 | 143 | /@algolia/transporter@4.20.0: 144 | resolution: {integrity: sha512-Lsii1pGWOAISbzeyuf+r/GPhvHMPHSPrTDWNcIzOE1SG1inlJHICaVe2ikuoRjcpgxZNU54Jl+if15SUCsaTUg==} 145 | dependencies: 146 | '@algolia/cache-common': 4.20.0 147 | '@algolia/logger-common': 4.20.0 148 | '@algolia/requester-common': 4.20.0 149 | dev: true 150 | 151 | /@babel/helper-string-parser@7.22.5: 152 | resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} 153 | engines: {node: '>=6.9.0'} 154 | dev: true 155 | 156 | /@babel/helper-validator-identifier@7.22.20: 157 | resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} 158 | engines: {node: '>=6.9.0'} 159 | dev: true 160 | 161 | /@babel/parser@7.23.0: 162 | resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} 163 | engines: {node: '>=6.0.0'} 164 | hasBin: true 165 | dependencies: 166 | '@babel/types': 7.23.0 167 | dev: true 168 | 169 | /@babel/types@7.23.0: 170 | resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} 171 | engines: {node: '>=6.9.0'} 172 | dependencies: 173 | '@babel/helper-string-parser': 7.22.5 174 | '@babel/helper-validator-identifier': 7.22.20 175 | to-fast-properties: 2.0.0 176 | dev: true 177 | 178 | /@docsearch/css@3.5.2: 179 | resolution: {integrity: sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==} 180 | dev: true 181 | 182 | /@docsearch/js@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0): 183 | resolution: {integrity: sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==} 184 | dependencies: 185 | '@docsearch/react': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0) 186 | preact: 10.18.1 187 | transitivePeerDependencies: 188 | - '@algolia/client-search' 189 | - '@types/react' 190 | - react 191 | - react-dom 192 | - search-insights 193 | dev: true 194 | 195 | /@docsearch/react@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0): 196 | resolution: {integrity: sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==} 197 | peerDependencies: 198 | '@types/react': '>= 16.8.0 < 19.0.0' 199 | react: '>= 16.8.0 < 19.0.0' 200 | react-dom: '>= 16.8.0 < 19.0.0' 201 | search-insights: '>= 1 < 3' 202 | peerDependenciesMeta: 203 | '@types/react': 204 | optional: true 205 | react: 206 | optional: true 207 | react-dom: 208 | optional: true 209 | search-insights: 210 | optional: true 211 | dependencies: 212 | '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0) 213 | '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0) 214 | '@docsearch/css': 3.5.2 215 | algoliasearch: 4.20.0 216 | search-insights: 2.9.0 217 | transitivePeerDependencies: 218 | - '@algolia/client-search' 219 | dev: true 220 | 221 | /@esbuild/android-arm64@0.18.20: 222 | resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} 223 | engines: {node: '>=12'} 224 | cpu: [arm64] 225 | os: [android] 226 | requiresBuild: true 227 | dev: true 228 | optional: true 229 | 230 | /@esbuild/android-arm@0.18.20: 231 | resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} 232 | engines: {node: '>=12'} 233 | cpu: [arm] 234 | os: [android] 235 | requiresBuild: true 236 | dev: true 237 | optional: true 238 | 239 | /@esbuild/android-x64@0.18.20: 240 | resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} 241 | engines: {node: '>=12'} 242 | cpu: [x64] 243 | os: [android] 244 | requiresBuild: true 245 | dev: true 246 | optional: true 247 | 248 | /@esbuild/darwin-arm64@0.18.20: 249 | resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} 250 | engines: {node: '>=12'} 251 | cpu: [arm64] 252 | os: [darwin] 253 | requiresBuild: true 254 | dev: true 255 | optional: true 256 | 257 | /@esbuild/darwin-x64@0.18.20: 258 | resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} 259 | engines: {node: '>=12'} 260 | cpu: [x64] 261 | os: [darwin] 262 | requiresBuild: true 263 | dev: true 264 | optional: true 265 | 266 | /@esbuild/freebsd-arm64@0.18.20: 267 | resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} 268 | engines: {node: '>=12'} 269 | cpu: [arm64] 270 | os: [freebsd] 271 | requiresBuild: true 272 | dev: true 273 | optional: true 274 | 275 | /@esbuild/freebsd-x64@0.18.20: 276 | resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} 277 | engines: {node: '>=12'} 278 | cpu: [x64] 279 | os: [freebsd] 280 | requiresBuild: true 281 | dev: true 282 | optional: true 283 | 284 | /@esbuild/linux-arm64@0.18.20: 285 | resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} 286 | engines: {node: '>=12'} 287 | cpu: [arm64] 288 | os: [linux] 289 | requiresBuild: true 290 | dev: true 291 | optional: true 292 | 293 | /@esbuild/linux-arm@0.18.20: 294 | resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} 295 | engines: {node: '>=12'} 296 | cpu: [arm] 297 | os: [linux] 298 | requiresBuild: true 299 | dev: true 300 | optional: true 301 | 302 | /@esbuild/linux-ia32@0.18.20: 303 | resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} 304 | engines: {node: '>=12'} 305 | cpu: [ia32] 306 | os: [linux] 307 | requiresBuild: true 308 | dev: true 309 | optional: true 310 | 311 | /@esbuild/linux-loong64@0.18.20: 312 | resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} 313 | engines: {node: '>=12'} 314 | cpu: [loong64] 315 | os: [linux] 316 | requiresBuild: true 317 | dev: true 318 | optional: true 319 | 320 | /@esbuild/linux-mips64el@0.18.20: 321 | resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} 322 | engines: {node: '>=12'} 323 | cpu: [mips64el] 324 | os: [linux] 325 | requiresBuild: true 326 | dev: true 327 | optional: true 328 | 329 | /@esbuild/linux-ppc64@0.18.20: 330 | resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} 331 | engines: {node: '>=12'} 332 | cpu: [ppc64] 333 | os: [linux] 334 | requiresBuild: true 335 | dev: true 336 | optional: true 337 | 338 | /@esbuild/linux-riscv64@0.18.20: 339 | resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} 340 | engines: {node: '>=12'} 341 | cpu: [riscv64] 342 | os: [linux] 343 | requiresBuild: true 344 | dev: true 345 | optional: true 346 | 347 | /@esbuild/linux-s390x@0.18.20: 348 | resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} 349 | engines: {node: '>=12'} 350 | cpu: [s390x] 351 | os: [linux] 352 | requiresBuild: true 353 | dev: true 354 | optional: true 355 | 356 | /@esbuild/linux-x64@0.18.20: 357 | resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} 358 | engines: {node: '>=12'} 359 | cpu: [x64] 360 | os: [linux] 361 | requiresBuild: true 362 | dev: true 363 | optional: true 364 | 365 | /@esbuild/netbsd-x64@0.18.20: 366 | resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} 367 | engines: {node: '>=12'} 368 | cpu: [x64] 369 | os: [netbsd] 370 | requiresBuild: true 371 | dev: true 372 | optional: true 373 | 374 | /@esbuild/openbsd-x64@0.18.20: 375 | resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} 376 | engines: {node: '>=12'} 377 | cpu: [x64] 378 | os: [openbsd] 379 | requiresBuild: true 380 | dev: true 381 | optional: true 382 | 383 | /@esbuild/sunos-x64@0.18.20: 384 | resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} 385 | engines: {node: '>=12'} 386 | cpu: [x64] 387 | os: [sunos] 388 | requiresBuild: true 389 | dev: true 390 | optional: true 391 | 392 | /@esbuild/win32-arm64@0.18.20: 393 | resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} 394 | engines: {node: '>=12'} 395 | cpu: [arm64] 396 | os: [win32] 397 | requiresBuild: true 398 | dev: true 399 | optional: true 400 | 401 | /@esbuild/win32-ia32@0.18.20: 402 | resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} 403 | engines: {node: '>=12'} 404 | cpu: [ia32] 405 | os: [win32] 406 | requiresBuild: true 407 | dev: true 408 | optional: true 409 | 410 | /@esbuild/win32-x64@0.18.20: 411 | resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} 412 | engines: {node: '>=12'} 413 | cpu: [x64] 414 | os: [win32] 415 | requiresBuild: true 416 | dev: true 417 | optional: true 418 | 419 | /@jridgewell/sourcemap-codec@1.4.15: 420 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 421 | dev: true 422 | 423 | /@types/linkify-it@3.0.3: 424 | resolution: {integrity: sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==} 425 | dev: true 426 | 427 | /@types/markdown-it@13.0.2: 428 | resolution: {integrity: sha512-Tla7hH9oeXHOlJyBFdoqV61xWE9FZf/y2g+gFVwQ2vE1/eBzjUno5JCd3Hdb5oATve5OF6xNjZ/4VIZhVVx+hA==} 429 | dependencies: 430 | '@types/linkify-it': 3.0.3 431 | '@types/mdurl': 1.0.3 432 | dev: true 433 | 434 | /@types/mdurl@1.0.3: 435 | resolution: {integrity: sha512-T5k6kTXak79gwmIOaDF2UUQXFbnBE0zBUzF20pz7wDYu0RQMzWg+Ml/Pz50214NsFHBITkoi5VtdjFZnJ2ijjA==} 436 | dev: true 437 | 438 | /@types/web-bluetooth@0.0.18: 439 | resolution: {integrity: sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==} 440 | dev: true 441 | 442 | /@vue/compiler-core@3.3.4: 443 | resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} 444 | dependencies: 445 | '@babel/parser': 7.23.0 446 | '@vue/shared': 3.3.4 447 | estree-walker: 2.0.2 448 | source-map-js: 1.0.2 449 | dev: true 450 | 451 | /@vue/compiler-dom@3.3.4: 452 | resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} 453 | dependencies: 454 | '@vue/compiler-core': 3.3.4 455 | '@vue/shared': 3.3.4 456 | dev: true 457 | 458 | /@vue/compiler-sfc@3.3.4: 459 | resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} 460 | dependencies: 461 | '@babel/parser': 7.23.0 462 | '@vue/compiler-core': 3.3.4 463 | '@vue/compiler-dom': 3.3.4 464 | '@vue/compiler-ssr': 3.3.4 465 | '@vue/reactivity-transform': 3.3.4 466 | '@vue/shared': 3.3.4 467 | estree-walker: 2.0.2 468 | magic-string: 0.30.5 469 | postcss: 8.4.31 470 | source-map-js: 1.0.2 471 | dev: true 472 | 473 | /@vue/compiler-ssr@3.3.4: 474 | resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} 475 | dependencies: 476 | '@vue/compiler-dom': 3.3.4 477 | '@vue/shared': 3.3.4 478 | dev: true 479 | 480 | /@vue/devtools-api@6.5.1: 481 | resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==} 482 | dev: true 483 | 484 | /@vue/reactivity-transform@3.3.4: 485 | resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} 486 | dependencies: 487 | '@babel/parser': 7.23.0 488 | '@vue/compiler-core': 3.3.4 489 | '@vue/shared': 3.3.4 490 | estree-walker: 2.0.2 491 | magic-string: 0.30.5 492 | dev: true 493 | 494 | /@vue/reactivity@3.3.4: 495 | resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} 496 | dependencies: 497 | '@vue/shared': 3.3.4 498 | dev: true 499 | 500 | /@vue/runtime-core@3.3.4: 501 | resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} 502 | dependencies: 503 | '@vue/reactivity': 3.3.4 504 | '@vue/shared': 3.3.4 505 | dev: true 506 | 507 | /@vue/runtime-dom@3.3.4: 508 | resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} 509 | dependencies: 510 | '@vue/runtime-core': 3.3.4 511 | '@vue/shared': 3.3.4 512 | csstype: 3.1.2 513 | dev: true 514 | 515 | /@vue/server-renderer@3.3.4(vue@3.3.4): 516 | resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} 517 | peerDependencies: 518 | vue: 3.3.4 519 | dependencies: 520 | '@vue/compiler-ssr': 3.3.4 521 | '@vue/shared': 3.3.4 522 | vue: 3.3.4 523 | dev: true 524 | 525 | /@vue/shared@3.3.4: 526 | resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} 527 | dev: true 528 | 529 | /@vueuse/core@10.5.0(vue@3.3.4): 530 | resolution: {integrity: sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==} 531 | dependencies: 532 | '@types/web-bluetooth': 0.0.18 533 | '@vueuse/metadata': 10.5.0 534 | '@vueuse/shared': 10.5.0(vue@3.3.4) 535 | vue-demi: 0.14.6(vue@3.3.4) 536 | transitivePeerDependencies: 537 | - '@vue/composition-api' 538 | - vue 539 | dev: true 540 | 541 | /@vueuse/integrations@10.5.0(focus-trap@7.5.4)(vue@3.3.4): 542 | resolution: {integrity: sha512-fm5sXLCK0Ww3rRnzqnCQRmfjDURaI4xMsx+T+cec0ngQqHx/JgUtm8G0vRjwtonIeTBsH1Q8L3SucE+7K7upJQ==} 543 | peerDependencies: 544 | async-validator: '*' 545 | axios: '*' 546 | change-case: '*' 547 | drauu: '*' 548 | focus-trap: '*' 549 | fuse.js: '*' 550 | idb-keyval: '*' 551 | jwt-decode: '*' 552 | nprogress: '*' 553 | qrcode: '*' 554 | sortablejs: '*' 555 | universal-cookie: '*' 556 | peerDependenciesMeta: 557 | async-validator: 558 | optional: true 559 | axios: 560 | optional: true 561 | change-case: 562 | optional: true 563 | drauu: 564 | optional: true 565 | focus-trap: 566 | optional: true 567 | fuse.js: 568 | optional: true 569 | idb-keyval: 570 | optional: true 571 | jwt-decode: 572 | optional: true 573 | nprogress: 574 | optional: true 575 | qrcode: 576 | optional: true 577 | sortablejs: 578 | optional: true 579 | universal-cookie: 580 | optional: true 581 | dependencies: 582 | '@vueuse/core': 10.5.0(vue@3.3.4) 583 | '@vueuse/shared': 10.5.0(vue@3.3.4) 584 | focus-trap: 7.5.4 585 | vue-demi: 0.14.6(vue@3.3.4) 586 | transitivePeerDependencies: 587 | - '@vue/composition-api' 588 | - vue 589 | dev: true 590 | 591 | /@vueuse/metadata@10.5.0: 592 | resolution: {integrity: sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==} 593 | dev: true 594 | 595 | /@vueuse/shared@10.5.0(vue@3.3.4): 596 | resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==} 597 | dependencies: 598 | vue-demi: 0.14.6(vue@3.3.4) 599 | transitivePeerDependencies: 600 | - '@vue/composition-api' 601 | - vue 602 | dev: true 603 | 604 | /algoliasearch@4.20.0: 605 | resolution: {integrity: sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g==} 606 | dependencies: 607 | '@algolia/cache-browser-local-storage': 4.20.0 608 | '@algolia/cache-common': 4.20.0 609 | '@algolia/cache-in-memory': 4.20.0 610 | '@algolia/client-account': 4.20.0 611 | '@algolia/client-analytics': 4.20.0 612 | '@algolia/client-common': 4.20.0 613 | '@algolia/client-personalization': 4.20.0 614 | '@algolia/client-search': 4.20.0 615 | '@algolia/logger-common': 4.20.0 616 | '@algolia/logger-console': 4.20.0 617 | '@algolia/requester-browser-xhr': 4.20.0 618 | '@algolia/requester-common': 4.20.0 619 | '@algolia/requester-node-http': 4.20.0 620 | '@algolia/transporter': 4.20.0 621 | dev: true 622 | 623 | /ansi-sequence-parser@1.1.1: 624 | resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} 625 | dev: true 626 | 627 | /csstype@3.1.2: 628 | resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} 629 | dev: true 630 | 631 | /esbuild@0.18.20: 632 | resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} 633 | engines: {node: '>=12'} 634 | hasBin: true 635 | requiresBuild: true 636 | optionalDependencies: 637 | '@esbuild/android-arm': 0.18.20 638 | '@esbuild/android-arm64': 0.18.20 639 | '@esbuild/android-x64': 0.18.20 640 | '@esbuild/darwin-arm64': 0.18.20 641 | '@esbuild/darwin-x64': 0.18.20 642 | '@esbuild/freebsd-arm64': 0.18.20 643 | '@esbuild/freebsd-x64': 0.18.20 644 | '@esbuild/linux-arm': 0.18.20 645 | '@esbuild/linux-arm64': 0.18.20 646 | '@esbuild/linux-ia32': 0.18.20 647 | '@esbuild/linux-loong64': 0.18.20 648 | '@esbuild/linux-mips64el': 0.18.20 649 | '@esbuild/linux-ppc64': 0.18.20 650 | '@esbuild/linux-riscv64': 0.18.20 651 | '@esbuild/linux-s390x': 0.18.20 652 | '@esbuild/linux-x64': 0.18.20 653 | '@esbuild/netbsd-x64': 0.18.20 654 | '@esbuild/openbsd-x64': 0.18.20 655 | '@esbuild/sunos-x64': 0.18.20 656 | '@esbuild/win32-arm64': 0.18.20 657 | '@esbuild/win32-ia32': 0.18.20 658 | '@esbuild/win32-x64': 0.18.20 659 | dev: true 660 | 661 | /estree-walker@2.0.2: 662 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 663 | dev: true 664 | 665 | /focus-trap@7.5.4: 666 | resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==} 667 | dependencies: 668 | tabbable: 6.2.0 669 | dev: true 670 | 671 | /fsevents@2.3.3: 672 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 673 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 674 | os: [darwin] 675 | requiresBuild: true 676 | dev: true 677 | optional: true 678 | 679 | /jsonc-parser@3.2.0: 680 | resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} 681 | dev: true 682 | 683 | /magic-string@0.30.5: 684 | resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} 685 | engines: {node: '>=12'} 686 | dependencies: 687 | '@jridgewell/sourcemap-codec': 1.4.15 688 | dev: true 689 | 690 | /mark.js@8.11.1: 691 | resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} 692 | dev: true 693 | 694 | /minisearch@6.1.0: 695 | resolution: {integrity: sha512-PNxA/X8pWk+TiqPbsoIYH0GQ5Di7m6326/lwU/S4mlo4wGQddIcf/V//1f9TB0V4j59b57b+HZxt8h3iMROGvg==} 696 | dev: true 697 | 698 | /nanoid@3.3.6: 699 | resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} 700 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 701 | hasBin: true 702 | dev: true 703 | 704 | /picocolors@1.0.0: 705 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 706 | dev: true 707 | 708 | /postcss@8.4.31: 709 | resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} 710 | engines: {node: ^10 || ^12 || >=14} 711 | dependencies: 712 | nanoid: 3.3.6 713 | picocolors: 1.0.0 714 | source-map-js: 1.0.2 715 | dev: true 716 | 717 | /preact@10.18.1: 718 | resolution: {integrity: sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==} 719 | dev: true 720 | 721 | /rollup@3.29.4: 722 | resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} 723 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 724 | hasBin: true 725 | optionalDependencies: 726 | fsevents: 2.3.3 727 | dev: true 728 | 729 | /search-insights@2.9.0: 730 | resolution: {integrity: sha512-bkWW9nIHOFkLwjQ1xqVaMbjjO5vhP26ERsH9Y3pKr8imthofEFIxlnOabkmGcw6ksRj9jWidcI65vvjJH/nTGg==} 731 | dev: true 732 | 733 | /shiki@0.14.5: 734 | resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==} 735 | dependencies: 736 | ansi-sequence-parser: 1.1.1 737 | jsonc-parser: 3.2.0 738 | vscode-oniguruma: 1.7.0 739 | vscode-textmate: 8.0.0 740 | dev: true 741 | 742 | /source-map-js@1.0.2: 743 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 744 | engines: {node: '>=0.10.0'} 745 | dev: true 746 | 747 | /tabbable@6.2.0: 748 | resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} 749 | dev: true 750 | 751 | /to-fast-properties@2.0.0: 752 | resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 753 | engines: {node: '>=4'} 754 | dev: true 755 | 756 | /vite@4.4.11: 757 | resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==} 758 | engines: {node: ^14.18.0 || >=16.0.0} 759 | hasBin: true 760 | peerDependencies: 761 | '@types/node': '>= 14' 762 | less: '*' 763 | lightningcss: ^1.21.0 764 | sass: '*' 765 | stylus: '*' 766 | sugarss: '*' 767 | terser: ^5.4.0 768 | peerDependenciesMeta: 769 | '@types/node': 770 | optional: true 771 | less: 772 | optional: true 773 | lightningcss: 774 | optional: true 775 | sass: 776 | optional: true 777 | stylus: 778 | optional: true 779 | sugarss: 780 | optional: true 781 | terser: 782 | optional: true 783 | dependencies: 784 | esbuild: 0.18.20 785 | postcss: 8.4.31 786 | rollup: 3.29.4 787 | optionalDependencies: 788 | fsevents: 2.3.3 789 | dev: true 790 | 791 | /vitepress@1.0.0-rc.22(@algolia/client-search@4.20.0)(search-insights@2.9.0): 792 | resolution: {integrity: sha512-n7le5iikCFgWMuX7sKfzDGJGlrsYQ5trG3S97BghNz2alOTr4Xp+GrB6ShwogUTX9gNgeNmrACjokhW55LNeBA==} 793 | hasBin: true 794 | peerDependencies: 795 | markdown-it-mathjax3: ^4.3.2 796 | postcss: ^8.4.31 797 | peerDependenciesMeta: 798 | markdown-it-mathjax3: 799 | optional: true 800 | postcss: 801 | optional: true 802 | dependencies: 803 | '@docsearch/css': 3.5.2 804 | '@docsearch/js': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.9.0) 805 | '@types/markdown-it': 13.0.2 806 | '@vue/devtools-api': 6.5.1 807 | '@vueuse/core': 10.5.0(vue@3.3.4) 808 | '@vueuse/integrations': 10.5.0(focus-trap@7.5.4)(vue@3.3.4) 809 | focus-trap: 7.5.4 810 | mark.js: 8.11.1 811 | minisearch: 6.1.0 812 | shiki: 0.14.5 813 | vite: 4.4.11 814 | vue: 3.3.4 815 | transitivePeerDependencies: 816 | - '@algolia/client-search' 817 | - '@types/node' 818 | - '@types/react' 819 | - '@vue/composition-api' 820 | - async-validator 821 | - axios 822 | - change-case 823 | - drauu 824 | - fuse.js 825 | - idb-keyval 826 | - jwt-decode 827 | - less 828 | - lightningcss 829 | - nprogress 830 | - qrcode 831 | - react 832 | - react-dom 833 | - sass 834 | - search-insights 835 | - sortablejs 836 | - stylus 837 | - sugarss 838 | - terser 839 | - universal-cookie 840 | dev: true 841 | 842 | /vscode-oniguruma@1.7.0: 843 | resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} 844 | dev: true 845 | 846 | /vscode-textmate@8.0.0: 847 | resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} 848 | dev: true 849 | 850 | /vue-demi@0.14.6(vue@3.3.4): 851 | resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} 852 | engines: {node: '>=12'} 853 | hasBin: true 854 | requiresBuild: true 855 | peerDependencies: 856 | '@vue/composition-api': ^1.0.0-rc.1 857 | vue: ^3.0.0-0 || ^2.6.0 858 | peerDependenciesMeta: 859 | '@vue/composition-api': 860 | optional: true 861 | dependencies: 862 | vue: 3.3.4 863 | dev: true 864 | 865 | /vue@3.3.4: 866 | resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} 867 | dependencies: 868 | '@vue/compiler-dom': 3.3.4 869 | '@vue/compiler-sfc': 3.3.4 870 | '@vue/runtime-dom': 3.3.4 871 | '@vue/server-renderer': 3.3.4(vue@3.3.4) 872 | '@vue/shared': 3.3.4 873 | dev: true 874 | --------------------------------------------------------------------------------