├── .clang-format ├── .github ├── FUNDING.yml └── workflows │ └── swift.yml ├── .gitignore ├── .justfile ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Ultraviolence │ ├── .github │ │ └── workflows │ │ │ └── swift.yml │ ├── .gitignore │ ├── .gitrepo │ ├── .pre-commit-config.yaml │ ├── .swiftlint.yml │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── .vscode │ │ └── launch.json │ ├── AnyBodylessElement.swift │ ├── AnyElement.swift │ ├── BlitPass.swift │ ├── CommandBufferElement.swift │ ├── ComputePass.swift │ ├── Core │ │ ├── BodylessContentElement.swift │ │ ├── BodylessElement.swift │ │ ├── ConditionalContent.swift │ │ ├── Dump.swift │ │ ├── Element.swift │ │ ├── ElementBuilder.swift │ │ ├── EmptyElement.swift │ │ ├── ForEach.swift │ │ ├── Graph.swift │ │ ├── Group.swift │ │ ├── Node.swift │ │ ├── Optional+Element.swift │ │ ├── StateBox.swift │ │ ├── TupleElement.swift │ │ ├── UVBinding.swift │ │ ├── UVEnvironmentValues.swift │ │ ├── UVObservedObject.swift │ │ └── UVState.swift │ ├── DebugLabelModifier.swift │ ├── Draw.swift │ ├── Graph+Process.swift │ ├── LoggingElement.swift │ ├── MetalFXSpatial.swift │ ├── Modifier.swift │ ├── ParameterValue.swift │ ├── Parameters.swift │ ├── Reflection.swift │ ├── RenderPass.swift │ ├── RenderPassDescriptorModifier.swift │ ├── RenderPipeline.swift │ ├── RenderPipelineDescriptorModifier.swift │ ├── Roots │ │ ├── Element+Run.swift │ │ └── OffscreenRenderer.swift │ ├── ShaderLibrary.swift │ ├── Shaders.swift │ ├── Support │ │ ├── Box.swift │ │ ├── Logging.swift │ │ ├── Support.swift │ │ ├── Visitation.swift │ │ ├── WeakBox.swift │ │ └── isEqual.swift │ ├── UVEnvironmentValues+Implementation.swift │ └── WorkloadModifier.swift ├── UltraviolenceMacros │ ├── UVEntryMacro.swift │ └── UltraviolenceMacros.swift ├── UltraviolenceSupport │ ├── BaseSupport.swift │ ├── Error.swift │ ├── Logging.swift │ ├── MetalSupport.swift │ └── UltraviolenceSupportMacros.swift └── UltraviolenceUI │ ├── Logging.swift │ ├── MTKView+Environment.swift │ ├── Parameter+SwiftUI.swift │ ├── RenderView.swift │ ├── RenderViewDebugViewModifier.swift │ └── ViewAdaptor.swift └── Tests ├── .swiftlint.yml └── UltraviolenceTests ├── CoreTests.swift ├── Golden Images └── RedTriangle.png ├── MacroTests.swift ├── RenderTests.swift └── Support.swift /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | #BasedOnStyle: LLVM 3 | AlignAfterOpenBracket: BlockIndent 4 | AlignTrailingComments: Always 5 | AllowShortBlocksOnASingleLine: Never 6 | AllowShortCaseExpressionOnASingleLine: false 7 | AllowShortCaseLabelsOnASingleLine: false 8 | AllowShortFunctionsOnASingleLine: None 9 | AllowShortIfStatementsOnASingleLine: Never 10 | BinPackParameters: OnePerLine 11 | BreakAfterAttributes: Never 12 | ColumnLimit: 120 13 | FixNamespaceComments: true 14 | IndentWidth: 4 15 | NamespaceIndentation: All 16 | PointerAlignment: Right 17 | ReferenceAlignment: Right 18 | RemoveBracesLLVM: false 19 | SortIncludes: true 20 | WrapNamespaceBodyWithEmptyLines: Always 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [schwa] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | env: 6 | XCODE_VERSION: "latest-stable" 7 | on: 8 | push: 9 | pull_request: 10 | jobs: 11 | swift-build: 12 | runs-on: macos-15 # macos-latest 13 | steps: 14 | - uses: maxim-lobanov/setup-xcode@v1 15 | with: 16 | xcode-version: ${{ env.XCODE_VERSION }} 17 | - uses: actions/checkout@v3 18 | - name: Trust plugins 19 | run: defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidation -bool YES 20 | - run: swift build -v 21 | - run: swift test -v 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | # .swiftpm 31 | 32 | .build/ 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | # 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | -------------------------------------------------------------------------------- /.justfile: -------------------------------------------------------------------------------- 1 | # XCODE_PROJECT_PATH := "./Demo/UltraviolenceDemo.xcodeproj" 2 | XCODE_SCHEME := "UltraviolenceDemo" 3 | CONFIGURATION := "Debug" 4 | 5 | default: list 6 | 7 | list: 8 | just --list 9 | 10 | 11 | build: 12 | swift build --quiet 13 | @echo "✅ Build Success" 14 | 15 | test: 16 | swift test --quiet 17 | @echo "✅ Test Success" 18 | 19 | push: build test periphery-scan 20 | jj bookmark move main --to @-; jj git push --branch main 21 | 22 | format: 23 | swiftlint --fix --format --quiet 24 | 25 | fd --extension metal --extension h --exec clang-format -i {} 26 | 27 | metal-nm: 28 | swift build --quiet 29 | # xcrun metal-nm .build/arm64-apple-macosx/debug/Ultraviolence_UltraviolenceExamples.bundle/debug.metallib 30 | #xcrun metal-objdump --disassemble-all .build/arm64-apple-macosx/debug/Ultraviolence_UltraviolenceExamples.bundle/debug.metallib 31 | #xcrun metal-source .build/arm64-apple-macosx/debug/Ultraviolence_UltraviolenceExamples.bundle/debug.metallib 32 | 33 | # periphery-scan-clean: 34 | # periphery scan --project-root Demo --project UltraviolenceDemo.xcodeproj --schemes UltraviolenceDemo --quiet --write-baseline .periphery.baseline.json --retain-public 35 | 36 | periphery-scan: 37 | # periphery scan --project-root Demo --project UltraviolenceDemo.xcodeproj --schemes UltraviolenceDemo --quiet --baseline .periphery.baseline.json --write-baseline .periphery.baseline.json --strict --retain-public 38 | # @echo "✅ periphery-scan Success" 39 | @echo "‼️ periphery-scan Skipped" 40 | 41 | create-todo-tickets: 42 | #!/usr/bin/env fish 43 | set RESULTS (rg "TODO: (?!\s*#\d)" -n --pcre2 --json --type-add 'code:*.{,swift,metal,h}' | jq -c '. | select(.type=="match")') 44 | 45 | for RESULT in $RESULTS 46 | # Extract file path, line number, and the TODO text 47 | set FILE_PATH (echo $RESULT | jq -r '.data.path.text') 48 | set LINE_NUMBER (echo $RESULT | jq -r '.data.line_number') 49 | set TODO_TEXT (echo $RESULT | jq -r '.data.lines.text' | string trim) 50 | 51 | echo "Processing TODO in $FILE_PATH at line $LINE_NUMBER: $TODO_TEXT" 52 | 53 | # Create a GitHub issue and capture the issue URL 54 | set ISSUE_URL (gh issue create --title "TODO: $TODO_TEXT" --body "Found in $FILE_PATH at line $LINE_NUMBER" | tee /dev/tty) 55 | 56 | # Extract the issue number from the URL 57 | set ISSUE_NUMBER (echo $ISSUE_URL | string replace -r '.*/issues/(\d+)' '$1') 58 | 59 | echo "Created issue #$ISSUE_NUMBER for TODO" 60 | 61 | # Modify the file by appending the issue number to the TODO 62 | set TEMP_FILE (mktemp) 63 | 64 | awk -v LINE_NUM=$LINE_NUMBER -v ISSUE_NUM=$ISSUE_NUMBER ' 65 | NR==LINE_NUM { 66 | sub(/TODO: /, "TODO: #" ISSUE_NUM " ", $0); 67 | } 68 | { print } 69 | ' "$FILE_PATH" > "$TEMP_FILE" && mv "$TEMP_FILE" "$FILE_PATH" 70 | 71 | echo "Updated TODO in $FILE_PATH to reference issue #$ISSUE_NUMBER" 72 | end 73 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | line_length: false 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | default_install_hook_types: 4 | - pre-commit 5 | - pre-push 6 | repos: 7 | - repo: local 8 | hooks: 9 | - id: swiftlint 10 | name: swiftlint lint 11 | entry: /opt/homebrew/bin/swiftlint lint --quiet 12 | language: system 13 | types: [swift] 14 | - id: swiftlint-fix 15 | name: swiftlint lint --fix 16 | entry: /opt/homebrew/bin/swiftlint lint --quiet --fix 17 | language: system 18 | types: [swift] 19 | - id: swift-test 20 | name: swift-test 21 | entry: swift test 22 | language: system 23 | types: [swift] 24 | stages: [pre-push] 25 | pass_filenames: false 26 | - id: xcodebuild 27 | name: xcodebuild 28 | entry: cd Demo && xcodebuild -scheme Ultraviolet && xcodebuild -scheme UltraviolenceDemo -destination 'platform=iOS Simulator,name=iPhone 16 Plus' build 29 | 30 | language: system 31 | types: [swift] 32 | stages: [pre-push] 33 | pass_filenames: false 34 | - repo: https://github.com/pre-commit/pre-commit-hooks 35 | rev: v5.0.0 36 | hooks: 37 | - id: check-case-conflict 38 | - id: check-executables-have-shebangs 39 | - id: check-json 40 | - id: check-merge-conflict 41 | - id: check-symlinks 42 | - id: check-toml 43 | - id: check-xml 44 | - id: check-yaml 45 | - id: end-of-file-fixer 46 | - id: trailing-whitespace 47 | # - id: check-shebang-scripts-are-executable 48 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | excluded: 3 | - "**/.build" 4 | - "**/Package.swift" 5 | force_cast: warning 6 | force_try: warning 7 | # line_length: 8 | # warning: 200 9 | # error: 10000 10 | # ignores_comments: true 11 | # ignores_function_declarations: true 12 | # ignores_urls: true 13 | shorthand_operator: warning 14 | type_body_length: 15 | warning: 250 16 | error: 10000 17 | type_name: 18 | allowed_symbols: ["_"] 19 | only_rules: 20 | - accessibility_label_for_image 21 | - accessibility_trait_for_button 22 | - anonymous_argument_in_multiline_closure 23 | - array_init 24 | - async_without_await 25 | - attribute_name_spacing 26 | # - attributes # 29 violations 27 | - balanced_xctest_lifecycle 28 | # - blanket_disable_command # 3 violations 29 | - block_based_kvo 30 | - capture_variable 31 | - class_delegate_protocol 32 | - closing_brace 33 | #- closure_body_length 34 | - closure_end_indentation 35 | - closure_parameter_position 36 | - closure_spacing 37 | - collection_alignment 38 | - colon 39 | - comma 40 | - comma_inheritance 41 | - comment_spacing 42 | - compiler_protocol_init 43 | - computed_accessors_order 44 | # - conditional_returns_on_newline 45 | - contains_over_filter_count 46 | - contains_over_filter_is_empty 47 | - contains_over_first_not_nil 48 | - contains_over_range_nil_comparison 49 | # - contrasted_opening_brace # 274 violations (fixable) 50 | - control_statement 51 | - convenience_type 52 | - custom_rules 53 | - cyclomatic_complexity 54 | - deployment_target 55 | - direct_return 56 | - discarded_notification_center_observer 57 | - discouraged_assert 58 | - discouraged_direct_init 59 | # - discouraged_none_name # 1 violations 60 | - discouraged_object_literal 61 | # - discouraged_optional_boolean 62 | - discouraged_optional_collection 63 | - duplicate_conditions 64 | - duplicate_enum_cases 65 | - duplicate_imports 66 | - duplicated_key_in_dictionary_literal 67 | - dynamic_inline 68 | - empty_collection_literal 69 | - empty_count 70 | - empty_enum_arguments 71 | - empty_parameters 72 | - empty_parentheses_with_trailing_closure 73 | - empty_string 74 | - empty_xctest_method 75 | - enum_case_associated_values_count 76 | - expiring_todo 77 | # - explicit_acl # 270 violations 78 | # - explicit_enum_raw_value 79 | - explicit_init 80 | - explicit_self 81 | - explicit_top_level_acl 82 | # - explicit_type_interface # 89 violations 83 | - extension_access_modifier 84 | - fallthrough 85 | - fatal_error_message 86 | - file_header 87 | - file_length 88 | # - file_name # 11 violations 89 | - file_name_no_space 90 | # - file_types_order # 38 violations 91 | - final_test_case 92 | - first_where 93 | - flatmap_over_map_reduce 94 | - for_where 95 | - force_cast 96 | - force_try 97 | - force_unwrapping 98 | - function_body_length 99 | # - function_default_parameter_at_end # 4 violations 100 | - function_parameter_count 101 | - generic_type_name 102 | - ibinspectable_in_extension 103 | - identical_operands 104 | # - identifier_name # 52 violations 105 | - implicit_getter 106 | - implicit_return 107 | - implicitly_unwrapped_optional 108 | - inclusive_language 109 | - indentation_width 110 | - invalid_swiftlint_command 111 | - is_disjoint 112 | - joined_default_parameter 113 | # - large_tuple # 1 violations 114 | - last_where 115 | - leading_whitespace 116 | - legacy_cggeometry_functions 117 | - legacy_constant 118 | - legacy_constructor 119 | - legacy_hashing 120 | - legacy_multiple 121 | - legacy_nsgeometry_functions 122 | - legacy_objc_type 123 | - legacy_random 124 | # - let_var_whitespace # 9 violations 125 | # - line_length # 127 violations 126 | - literal_expression_end_indentation 127 | - local_doc_comment 128 | - lower_acl_than_parent 129 | - mark 130 | # - missing_docs # 186 violations 131 | - modifier_order 132 | - multiline_arguments 133 | - multiline_arguments_brackets 134 | - multiline_function_chains 135 | - multiline_literal_brackets 136 | - multiline_parameters 137 | - multiline_parameters_brackets 138 | - multiple_closures_with_trailing_closure 139 | - nesting 140 | - nimble_operator 141 | - no_empty_block 142 | # - no_extension_access_modifier # 70 violations 143 | - no_fallthrough_only 144 | # - no_grouping_extension # 34 violations 145 | # - no_magic_numbers # 148 violations 146 | - no_space_in_method_call 147 | - non_optional_string_data_conversion 148 | - non_overridable_class_declaration 149 | - notification_center_detachment 150 | - ns_number_init_as_function_reference 151 | - nslocalizedstring_key 152 | - nslocalizedstring_require_bundle 153 | - nsobject_prefer_isequal 154 | - number_separator 155 | - object_literal 156 | # - one_declaration_per_file # 32 violations 157 | - opening_brace 158 | - operator_usage_whitespace 159 | # - operator_whitespace 160 | - optional_data_string_conversion 161 | - optional_enum_case_matching 162 | - orphaned_doc_comment 163 | - overridden_super_call 164 | - override_in_extension 165 | # - pattern_matching_keywords # 6 violations 166 | - period_spacing 167 | - prefer_key_path 168 | - prefer_nimble 169 | - prefer_self_in_static_references 170 | - prefer_self_type_over_type_of_self 171 | - prefer_type_checking 172 | - prefer_zero_over_explicit_init 173 | # - prefixed_toplevel_constant # 6 violations 174 | - private_action 175 | - private_outlet 176 | - private_over_fileprivate 177 | - private_subject 178 | - private_swiftui_state 179 | - private_unit_test 180 | - prohibited_interface_builder 181 | - prohibited_super_call 182 | - protocol_property_accessors_order 183 | - quick_discouraged_call 184 | - quick_discouraged_focused_test 185 | - quick_discouraged_pending_test 186 | - raw_value_for_camel_cased_codable_enum 187 | - reduce_boolean 188 | - reduce_into 189 | - redundant_discardable_let 190 | - redundant_nil_coalescing 191 | - redundant_objc_attribute 192 | - redundant_optional_initialization 193 | - redundant_self_in_closure 194 | - redundant_sendable 195 | - redundant_set_access_control 196 | - redundant_string_enum_value 197 | - redundant_type_annotation 198 | - redundant_void_return 199 | # - required_deinit # 10 violations 200 | - required_enum_case 201 | - return_arrow_whitespace 202 | - return_value_from_void_function 203 | - self_binding 204 | - self_in_property_initialization 205 | - shorthand_argument 206 | - shorthand_operator 207 | - shorthand_optional_binding 208 | - single_test_class 209 | # - sorted_enum_cases # 12 violations 210 | - sorted_first_last 211 | - sorted_imports # 20 violations (fixable) 212 | # - statement_position 213 | - static_operator 214 | - static_over_final_class 215 | # - strict_fileprivate 216 | - strong_iboutlet 217 | - superfluous_disable_command 218 | - superfluous_else 219 | - switch_case_alignment 220 | - switch_case_on_newline 221 | - syntactic_sugar 222 | - test_case_accessibility 223 | # - todo # 49 violations 224 | - toggle_bool 225 | - trailing_closure 226 | - trailing_comma 227 | - trailing_newline 228 | - trailing_semicolon 229 | - trailing_whitespace 230 | - type_body_length 231 | # - type_contents_order # 22 violations 232 | - type_name 233 | - typesafe_array_init 234 | - unavailable_condition 235 | - unavailable_function 236 | - unhandled_throwing_task 237 | - unneeded_break_in_switch 238 | - unneeded_override 239 | - unneeded_parentheses_in_closure_argument 240 | - unneeded_synthesized_initializer 241 | - unowned_variable_capture 242 | - untyped_error_in_catch 243 | - unused_closure_parameter 244 | - unused_control_flow_label 245 | - unused_declaration 246 | - unused_enumerated 247 | - unused_import 248 | - unused_optional_binding 249 | # - unused_parameter # 9 violations (fixable) 250 | # - unused_setter_value # 1 violations 251 | - valid_ibinspectable 252 | - vertical_parameter_alignment 253 | - vertical_parameter_alignment_on_call 254 | - vertical_whitespace 255 | # - vertical_whitespace_between_cases 256 | - vertical_whitespace_closing_braces 257 | - vertical_whitespace_opening_braces 258 | - void_function_in_ternary 259 | - void_return 260 | - weak_delegate 261 | - xct_specific_matcher 262 | - xctfail_message 263 | - yoda_condition 264 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "lldb", 5 | "request": "launch", 6 | "args": [], 7 | "cwd": "${workspaceFolder:Ultraviolence}", 8 | "name": "Debug uvcli", 9 | "program": "${workspaceFolder:Ultraviolence}/.build/debug/uvcli", 10 | "preLaunchTask": "swift: Build Debug uvcli" 11 | }, 12 | { 13 | "type": "lldb", 14 | "request": "launch", 15 | "args": [], 16 | "cwd": "${workspaceFolder:Ultraviolence}", 17 | "name": "Release uvcli", 18 | "program": "${workspaceFolder:Ultraviolence}/.build/release/uvcli", 19 | "preLaunchTask": "swift: Build Release uvcli" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Blit", 4 | "Objc", 5 | "stdlib", 6 | "Ultraviolence", 7 | "Upscaler" 8 | ], 9 | "cSpell.enableFiletypes": [ 10 | "!jsonc", 11 | "!swift" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jonathan Wight 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "b11b2f81704cc903c109a059777eb465eecf799c6b2bc255c5635b5e5c7bc500", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-syntax", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/swiftlang/swift-syntax.git", 8 | "state" : { 9 | "revision" : "0687f71944021d616d34d922343dcef086855920", 10 | "version" : "600.0.1" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import CompilerPluginSupport 4 | import PackageDescription 5 | 6 | public let package = Package( 7 | name: "Ultraviolence", 8 | platforms: [ 9 | .iOS(.v18), 10 | .macOS(.v15) 11 | ], 12 | products: [ 13 | .library(name: "Ultraviolence", targets: ["Ultraviolence"]), 14 | .library(name: "UltraviolenceUI", targets: ["UltraviolenceUI"]), 15 | .library(name: "UltraviolenceSupport", targets: ["UltraviolenceSupport"]) 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "Ultraviolence", 23 | dependencies: [ 24 | "UltraviolenceSupport" 25 | ] 26 | ), 27 | .target( 28 | name: "UltraviolenceUI", 29 | dependencies: [ 30 | "Ultraviolence", 31 | "UltraviolenceSupport" 32 | ] 33 | ), 34 | .target( 35 | name: "UltraviolenceSupport", 36 | dependencies: [ 37 | "UltraviolenceMacros" 38 | ] 39 | ), 40 | .macro( 41 | name: "UltraviolenceMacros", 42 | dependencies: [ 43 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 44 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 45 | ] 46 | ), 47 | .testTarget( 48 | name: "UltraviolenceTests", 49 | dependencies: [ 50 | "Ultraviolence", 51 | "UltraviolenceUI", 52 | "UltraviolenceSupport", 53 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax") 54 | ], 55 | resources: [ 56 | .copy("Golden Images") 57 | ] 58 | ) 59 | ], 60 | swiftLanguageModes: [.v6] 61 | ) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Ultraviolence](https://github.com/schwa/Ultraviolence) 2 | 3 | An _**experimental**_ declarative framework for Metal rendering in Swift. 4 | 5 | See [Wiki](https://github.com/schwa/Ultraviolence/wiki) for more information. 6 | 7 | See [Github Projects](https://github.com/users/schwa/projects/7) for current issues and tasks. 8 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: macos-15 13 | steps: 14 | - uses: maxim-lobanov/setup-xcode@v1 15 | with: 16 | xcode-version: 16 17 | - uses: actions/checkout@v3 18 | - name: Build 19 | run: swift build -v 20 | - name: Run tests 21 | run: swift test -v 22 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,swiftpm,xcode,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### macOS Patch ### 33 | # iCloud generated files 34 | *.icloud 35 | 36 | ### Swift ### 37 | # Xcode 38 | # 39 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 40 | 41 | ## User settings 42 | xcuserdata/ 43 | 44 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 45 | *.xcscmblueprint 46 | *.xccheckout 47 | 48 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 49 | build/ 50 | DerivedData/ 51 | *.moved-aside 52 | *.pbxuser 53 | !default.pbxuser 54 | *.mode1v3 55 | !default.mode1v3 56 | *.mode2v3 57 | !default.mode2v3 58 | *.perspectivev3 59 | !default.perspectivev3 60 | 61 | ## Obj-C/Swift specific 62 | *.hmap 63 | 64 | ## App packaging 65 | *.ipa 66 | *.dSYM.zip 67 | *.dSYM 68 | 69 | ## Playgrounds 70 | timeline.xctimeline 71 | playground.xcworkspace 72 | 73 | # Swift Package Manager 74 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 75 | # Packages/ 76 | # Package.pins 77 | # Package.resolved 78 | # *.xcodeproj 79 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 80 | # hence it is not needed unless you have added a package configuration file to your project 81 | # .swiftpm 82 | 83 | .build/ 84 | 85 | # CocoaPods 86 | # We recommend against adding the Pods directory to your .gitignore. However 87 | # you should judge for yourself, the pros and cons are mentioned at: 88 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 89 | # Pods/ 90 | # Add this line if you want to avoid checking in source code from the Xcode workspace 91 | # *.xcworkspace 92 | 93 | # Carthage 94 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 95 | # Carthage/Checkouts 96 | 97 | Carthage/Build/ 98 | 99 | # Accio dependency management 100 | Dependencies/ 101 | .accio/ 102 | 103 | # fastlane 104 | # It is recommended to not store the screenshots in the git repo. 105 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 106 | # For more information about the recommended setup visit: 107 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 108 | 109 | fastlane/report.xml 110 | fastlane/Preview.html 111 | fastlane/screenshots/**/*.png 112 | fastlane/test_output 113 | 114 | # Code Injection 115 | # After new code Injection tools there's a generated folder /iOSInjectionProject 116 | # https://github.com/johnno1962/injectionforxcode 117 | 118 | iOSInjectionProject/ 119 | 120 | ### SwiftPM ### 121 | Packages 122 | xcuserdata 123 | *.xcodeproj 124 | 125 | 126 | ### Xcode ### 127 | 128 | ## Xcode 8 and earlier 129 | 130 | ### Xcode Patch ### 131 | *.xcodeproj/* 132 | !*.xcodeproj/project.pbxproj 133 | !*.xcodeproj/xcshareddata/ 134 | !*.xcodeproj/project.xcworkspace/ 135 | !*.xcworkspace/contents.xcworkspacedata 136 | /*.gcno 137 | **/xcshareddata/WorkspaceSettings.xcsettings 138 | 139 | # End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode,macos 140 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/.gitrepo: -------------------------------------------------------------------------------- 1 | ; DO NOT EDIT (unless you know what you are doing) 2 | ; 3 | ; This subdirectory is a git "subrepo", and this file is maintained by the 4 | ; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme 5 | ; 6 | [subrepo] 7 | remote = https://github.com/schwa/NotSwiftUI 8 | branch = main 9 | commit = 28df1fa8886790c7f0d70af3661441ea24db7c62 10 | parent = 2b15aa2b30d49e0bbc42aa490161f10b4ed12793 11 | method = merge 12 | cmdver = 0.4.9 13 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | default_install_hook_types: 4 | - pre-commit 5 | - pre-push 6 | repos: 7 | - repo: local 8 | hooks: 9 | - id: swiftlint 10 | name: swiftlint lint 11 | entry: /opt/homebrew/bin/swiftlint lint --quiet 12 | language: system 13 | types: [swift] 14 | - id: swiftlint-fix 15 | name: swiftlint lint --fix 16 | entry: /opt/homebrew/bin/swiftlint lint --quiet --fix 17 | language: system 18 | types: [swift] 19 | - id: swift-test 20 | name: swift-test 21 | entry: swift test 22 | language: system 23 | types: [swift] 24 | stages: [pre-push] 25 | pass_filenames: false 26 | - repo: https://github.com/pre-commit/pre-commit-hooks 27 | rev: v5.0.0 28 | hooks: 29 | - id: check-case-conflict 30 | - id: check-executables-have-shebangs 31 | - id: check-json 32 | - id: check-merge-conflict 33 | - id: check-symlinks 34 | - id: check-toml 35 | - id: check-xml 36 | - id: check-yaml 37 | - id: end-of-file-fixer 38 | - id: trailing-whitespace 39 | # - id: check-shebang-scripts-are-executable 40 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | force_cast: 2 | severity: warning 3 | force_try: 4 | severity: warning 5 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "lldb", 5 | "request": "launch", 6 | "args": [], 7 | "cwd": "${workspaceFolder:NotSwiftUI}", 8 | "name": "Debug NotSwiftUIClient", 9 | "program": "${workspaceFolder:NotSwiftUI}/.build/debug/NotSwiftUIClient", 10 | "preLaunchTask": "swift: Build Debug NotSwiftUIClient" 11 | }, 12 | { 13 | "type": "lldb", 14 | "request": "launch", 15 | "args": [], 16 | "cwd": "${workspaceFolder:NotSwiftUI}", 17 | "name": "Release NotSwiftUIClient", 18 | "program": "${workspaceFolder:NotSwiftUI}/.build/release/NotSwiftUIClient", 19 | "preLaunchTask": "swift: Build Release NotSwiftUIClient" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/AnyBodylessElement.swift: -------------------------------------------------------------------------------- 1 | // TODO: #64 Experimental solution. Ergonmonics are bad however. 2 | internal struct AnyBodylessElement: Element, BodylessElement { 3 | fileprivate var _setupEnter: (() throws -> Void)? 4 | fileprivate var _setupExit: (() throws -> Void)? 5 | fileprivate var _workloadEnter: (() throws -> Void)? 6 | fileprivate var _workloadExit: (() throws -> Void)? 7 | 8 | init() { 9 | // This line intentionally left blank 10 | } 11 | 12 | func _expandNode(_: Node, context: ExpansionContext) throws { 13 | // This line intentionally left blank. 14 | } 15 | 16 | func setupEnter(_: Node) throws { 17 | try _setupEnter?() 18 | } 19 | 20 | func setupExit(_: Node) throws { 21 | try _setupExit?() 22 | } 23 | 24 | func workloadEnter(_: Node) throws { 25 | try _workloadEnter?() 26 | } 27 | 28 | func workloadExit(_: Node) throws { 29 | try _workloadExit?() 30 | } 31 | } 32 | 33 | internal extension AnyBodylessElement { 34 | func onSetupEnter(_ action: @escaping () throws -> Void) -> AnyBodylessElement { 35 | var modifier = self 36 | modifier._setupEnter = action 37 | return modifier 38 | } 39 | func onSetupExit(_ action: @escaping () throws -> Void) -> AnyBodylessElement { 40 | var modifier = self 41 | modifier._setupExit = action 42 | return modifier 43 | } 44 | func onWorkloadEnter(_ action: @escaping () throws -> Void) -> AnyBodylessElement { 45 | var modifier = self 46 | modifier._workloadEnter = action 47 | return modifier 48 | } 49 | func onWorkloadExit(_ action: @escaping () throws -> Void) -> AnyBodylessElement { 50 | var modifier = self 51 | modifier._workloadExit = action 52 | return modifier 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/AnyElement.swift: -------------------------------------------------------------------------------- 1 | public struct AnyElement: Element, BodylessElement { 2 | private let expand: (Node, ExpansionContext) throws -> Void 3 | 4 | public init(_ base: some Element) { 5 | expand = { node, context in 6 | try base.expandNode(node, context: context) 7 | } 8 | } 9 | 10 | internal func _expandNode(_ node: Node, context: ExpansionContext) throws { 11 | let graph = try node.graph.orThrow(.noCurrentGraph) 12 | if node.children.isEmpty { 13 | node.children.append(graph.makeNode()) 14 | } 15 | try expand(node.children[0], context.deeper()) 16 | } 17 | } 18 | 19 | public extension Element { 20 | func eraseToAnyElement() -> AnyElement { 21 | AnyElement(self) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/BlitPass.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | 3 | public struct BlitPass : Element, BodylessElement, BodylessContentElement where Content: Element { 4 | internal let content: Content 5 | 6 | public init(@ElementBuilder content: () throws -> Content) throws { 7 | self.content = try content() 8 | } 9 | 10 | func workloadEnter(_ node: Node) throws { 11 | let commandBuffer = try node.environmentValues.commandBuffer.orThrow(.missingEnvironment(\.commandBuffer)) 12 | let blitCommandEncoder = try commandBuffer._makeBlitCommandEncoder() 13 | node.environmentValues.blitCommandEncoder = blitCommandEncoder 14 | } 15 | 16 | func workloadExit(_ node: Node) throws { 17 | let blitCommandEncoder = try node.environmentValues.blitCommandEncoder.orThrow(.missingEnvironment(\.blitCommandEncoder)) 18 | blitCommandEncoder.endEncoding() 19 | } 20 | } 21 | 22 | public struct Blit: Element, BodylessElement { 23 | var block: (MTLBlitCommandEncoder) throws -> Void 24 | 25 | public init(_ block: @escaping (MTLBlitCommandEncoder) throws -> Void) { 26 | self.block = block 27 | } 28 | 29 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 30 | // This line intentionally left blank. 31 | } 32 | 33 | func workloadEnter(_ node: Node) throws { 34 | let blitCommandEncoder = try node.environmentValues.blitCommandEncoder.orThrow(.missingEnvironment(\.blitCommandEncoder)) 35 | try block(blitCommandEncoder) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/CommandBufferElement.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import UltraviolenceSupport 3 | 4 | // TODO: #46 Rename. Remove element from name. "CommandBuffer" is _not_ a good name though. 5 | public struct CommandBufferElement : Element, BodylessContentElement where Content: Element { 6 | // @UVEnvironment(\.enableMetalLogging) 7 | // var enableMetalLogging 8 | 9 | var completion: MTLCommandQueueCompletion 10 | var content: Content 11 | 12 | public init(completion: MTLCommandQueueCompletion, @ElementBuilder content: () throws -> Content) rethrows { 13 | self.completion = completion 14 | self.content = try content() 15 | } 16 | 17 | func workloadEnter(_ node: Node) throws { 18 | let commandQueue = try node.environmentValues.commandQueue.orThrow(.missingEnvironment(\.commandQueue)) 19 | let commandBufferDescriptor = MTLCommandBufferDescriptor() 20 | // TODO: #97 Users cannot modify the environment here. This is a problem. 21 | // if enableMetalLogging { 22 | // print("ENABLING LOGGING") 23 | // try commandBufferDescriptor.addDefaultLogging() 24 | // } 25 | // TODO: #98 There isn't an opportunity to modify the descriptor here. 26 | let commandBuffer = try commandQueue._makeCommandBuffer(descriptor: commandBufferDescriptor) 27 | node.environmentValues.commandBuffer = commandBuffer 28 | } 29 | 30 | func workloadExit(_ node: Node) throws { 31 | let commandBuffer = try node.environmentValues.commandBuffer.orThrow(.missingEnvironment(\.commandBuffer)) 32 | switch completion { 33 | case .none: 34 | break 35 | 36 | case .commit: 37 | commandBuffer.commit() 38 | 39 | case .commitAndWaitUntilCompleted: 40 | commandBuffer.commit() 41 | commandBuffer.waitUntilCompleted() 42 | } 43 | } 44 | } 45 | 46 | // MARK: - 47 | 48 | public extension Element { 49 | func onCommandBufferScheduled(_ action: @escaping (MTLCommandBuffer) -> Void) -> some Element { 50 | EnvironmentReader(keyPath: \.commandBuffer) { commandBuffer in 51 | self.onWorkloadEnter { _ in 52 | if let commandBuffer { 53 | commandBuffer.addScheduledHandler { commandBuffer in 54 | action(commandBuffer) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | func onCommandBufferCompleted(_ action: @escaping (MTLCommandBuffer) -> Void) -> some Element { 62 | EnvironmentReader(keyPath: \.commandBuffer) { commandBuffer in 63 | self.onWorkloadEnter { _ in 64 | if let commandBuffer { 65 | commandBuffer.addCompletedHandler { commandBuffer in 66 | action(commandBuffer) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/ComputePass.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import UltraviolenceSupport 3 | 4 | public struct ComputePass : Element, BodylessElement, BodylessContentElement where Content: Element { 5 | internal let content: Content 6 | 7 | public init(@ElementBuilder content: () throws -> Content) throws { 8 | self.content = try content() 9 | } 10 | 11 | func workloadEnter(_ node: Node) throws { 12 | let commandBuffer = try node.environmentValues.commandBuffer.orThrow(.missingEnvironment(\.commandBuffer)) 13 | let computeCommandEncoder = try commandBuffer._makeComputeCommandEncoder() 14 | node.environmentValues.computeCommandEncoder = computeCommandEncoder 15 | } 16 | 17 | func workloadExit(_ node: Node) throws { 18 | let computeCommandEncoder = try node.environmentValues.computeCommandEncoder.orThrow(.missingEnvironment(\.computeCommandEncoder)) 19 | computeCommandEncoder.endEncoding() 20 | } 21 | } 22 | 23 | // MARK: - 24 | 25 | public struct ComputePipeline : Element, BodylessElement, BodylessContentElement where Content: Element { 26 | var computeKernel: ComputeKernel 27 | var content: Content 28 | 29 | public init(computeKernel: ComputeKernel, @ElementBuilder content: () -> Content) { 30 | self.computeKernel = computeKernel 31 | self.content = content() 32 | } 33 | 34 | func setupEnter(_ node: Node) throws { 35 | let device = try node.environmentValues.device.orThrow(.missingEnvironment(\.device)) 36 | let descriptor = MTLComputePipelineDescriptor() 37 | descriptor.computeFunction = computeKernel.function 38 | let (computePipelineState, reflection) = try device.makeComputePipelineState(descriptor: descriptor, options: .bindingInfo) 39 | node.environmentValues.reflection = Reflection(try reflection.orThrow(.resourceCreationFailure("Failed to create reflection."))) 40 | node.environmentValues.computePipelineState = computePipelineState 41 | } 42 | } 43 | 44 | // MARK: - 45 | 46 | public struct ComputeDispatch: Element, BodylessElement { 47 | var threads: MTLSize 48 | var threadsPerThreadgroup: MTLSize 49 | 50 | public init(threads: MTLSize, threadsPerThreadgroup: MTLSize) { 51 | self.threads = threads 52 | self.threadsPerThreadgroup = threadsPerThreadgroup 53 | } 54 | 55 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 56 | // This line intentionally left blank. 57 | } 58 | 59 | func workloadEnter(_ node: Node) throws { 60 | guard let computeCommandEncoder = node.environmentValues.computeCommandEncoder, let computePipelineState = node.environmentValues.computePipelineState else { 61 | preconditionFailure("No compute command encoder/compute pipeline state found.") 62 | } 63 | computeCommandEncoder.setComputePipelineState(computePipelineState) 64 | // TODO: #117 Simulator problem `Dispatch Threads with Non-Uniform Threadgroup Size is not supported on this device' 65 | computeCommandEncoder.dispatchThreads(threads, threadsPerThreadgroup: threadsPerThreadgroup) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/BodylessContentElement.swift: -------------------------------------------------------------------------------- 1 | internal protocol BodylessContentElement: BodylessElement { 2 | associatedtype Content: Element 3 | 4 | var content: Content { get } 5 | } 6 | 7 | extension BodylessContentElement { 8 | func expandNodeHelper(_ node: Node, context: ExpansionContext) throws { 9 | let graph = try node.graph.orThrow(.noCurrentGraph) 10 | if node.children.isEmpty { 11 | node.children.append(graph.makeNode()) 12 | } 13 | try content.expandNode(node.children[0], context: context.deeper()) 14 | } 15 | 16 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 17 | try expandNodeHelper(node, context: context) 18 | } 19 | } 20 | 21 | internal struct ExpansionContext { 22 | var depth: Int 23 | 24 | init(depth: Int = 0) { 25 | self.depth = depth 26 | } 27 | } 28 | 29 | internal extension ExpansionContext { 30 | func deeper() -> ExpansionContext { 31 | ExpansionContext(depth: depth + 1) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/BodylessElement.swift: -------------------------------------------------------------------------------- 1 | @MainActor 2 | internal protocol BodylessElement { 3 | // TODO: #12 This should be renamed. And it should be differently named than Node.buildNodeTree. 4 | func _expandNode(_ node: Node, context: ExpansionContext) throws 5 | 6 | func setupEnter(_ node: Node) throws 7 | func setupExit(_ node: Node) throws 8 | 9 | func workloadEnter(_ node: Node) throws 10 | func workloadExit(_ node: Node) throws 11 | } 12 | 13 | internal extension BodylessElement { 14 | func setupEnter(_ node: Node) throws { 15 | // This line intentionally left blank. 16 | } 17 | 18 | func setupExit(_ node: Node) throws { 19 | // This line intentionally left blank. 20 | } 21 | 22 | func workloadEnter(_ node: Node) throws { 23 | // This line intentionally left blank. 24 | } 25 | 26 | func workloadExit(_ node: Node) throws { 27 | // This line intentionally left blank. 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/ConditionalContent.swift: -------------------------------------------------------------------------------- 1 | public struct _ConditionalContent: Element, BodylessElement where TrueContent: Element, FalseContent: Element { 2 | let first: TrueContent? 3 | let second: FalseContent? 4 | 5 | init(first: TrueContent) { 6 | self.first = first 7 | self.second = nil 8 | } 9 | 10 | init(second: FalseContent) { 11 | self.first = nil 12 | self.second = second 13 | } 14 | 15 | internal func _expandNode(_ node: Node, context: ExpansionContext) throws { 16 | if let first { 17 | try first.expandNode(node, context: context.deeper()) 18 | } 19 | else if let second { 20 | try second.expandNode(node, context: context.deeper()) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/Dump.swift: -------------------------------------------------------------------------------- 1 | public struct DumpOptions: OptionSet, Sendable { 2 | public let rawValue: Int 3 | 4 | public init(rawValue: Int) { 5 | self.rawValue = rawValue 6 | } 7 | 8 | public static let `default`: DumpOptions = [.dumpElement] 9 | public static let dumpElement = Self(rawValue: 1 << 0) 10 | public static let dumpNode = Self(rawValue: 1 << 1) 11 | } 12 | 13 | public extension Node { 14 | @MainActor 15 | func dump(options: DumpOptions = .default, to output: inout some TextOutputStream) throws { 16 | func dump(for node: Node) -> String { 17 | let elements: [String?] = [ 18 | "id: \(ObjectIdentifier(node))", 19 | node.parent == nil ? "no parent" : nil, 20 | node.children.isEmpty ? "no children" : "\(node.children.count) children", 21 | node.needsRebuild ? "needs rebuild" : nil, 22 | node.element == nil ? "no element" : nil, 23 | node.previousElement == nil ? "no previous element" : nil, 24 | "state: \(node.stateProperties.keys.joined(separator: "|"))", 25 | "env: \(node.environmentValues.storage.values.keys.map { "\($0)" }.joined(separator: "|"))", 26 | node.debugLabel == nil ? nil : "debug: \(node.debugLabel ?? "")" 27 | ] 28 | return elements.compactMap(\.self).joined(separator: ", ") 29 | } 30 | visit { depth, node in 31 | let indent = String(repeating: " ", count: depth) 32 | if options.contains(.dumpElement) { 33 | let element = node.element 34 | if let element { 35 | let typeName = String(describing: type(of: element)).split(separator: "<").first ?? "" 36 | print("\(indent)\(typeName)", terminator: "", to: &output) 37 | if options.contains(.dumpNode) { 38 | print(" (\(dump(for: node)))", terminator: "", to: &output) 39 | } 40 | print("", to: &output) 41 | } 42 | else { 43 | print("\(indent)", to: &output) 44 | } 45 | } 46 | } 47 | } 48 | 49 | @MainActor 50 | func dump(options: DumpOptions = .default) throws { 51 | var s = "" 52 | try dump(options: options, to: &s) 53 | print(s, terminator: "") 54 | } 55 | } 56 | 57 | // MARK: - 58 | 59 | public extension Graph { 60 | @MainActor 61 | func dump(options: DumpOptions = .default, to output: inout some TextOutputStream) throws { 62 | try rebuildIfNeeded() 63 | try root.dump(options: options, to: &output) 64 | } 65 | 66 | @MainActor 67 | func dump(options: DumpOptions = .default) throws { 68 | var s = "" 69 | try dump(options: options, to: &s) 70 | print(s, terminator: "") 71 | } 72 | } 73 | 74 | // MARK: - 75 | 76 | public extension Element { 77 | func dump(options: DumpOptions = .default, to output: inout some TextOutputStream) throws { 78 | let graph = try Graph(content: self) 79 | try graph.rebuildIfNeeded() 80 | try graph.dump(options: options, to: &output) 81 | } 82 | 83 | func dump(options: DumpOptions = .default) throws { 84 | var output = String() 85 | try dump(options: options, to: &output) 86 | print(output) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/Element.swift: -------------------------------------------------------------------------------- 1 | import UltraviolenceSupport 2 | 3 | @MainActor 4 | public protocol Element { 5 | associatedtype Body: Element 6 | @MainActor @ElementBuilder var body: Body { get throws } 7 | } 8 | 9 | extension Never: Element { 10 | public typealias Body = Never 11 | } 12 | 13 | public extension Element where Body == Never { 14 | var body: Never { 15 | unreachable("`body` is not implemented for `Never` types (on \(self)).") 16 | } 17 | } 18 | 19 | internal extension Element { 20 | func expandNode(_ node: Node, context: ExpansionContext) throws { 21 | // TODO: #23 Refactor this to make expansion of the node tree distinct from handling observable and state properties. 22 | guard let graph = Graph.current else { 23 | preconditionFailure("No graph is currently active.") 24 | } 25 | 26 | // TODO: #35 Avoid this in future 27 | let parent = graph.activeNodeStack.last !== node ? graph.activeNodeStack.last : nil 28 | 29 | graph.activeNodeStack.append(node) 30 | defer { 31 | _ = graph.activeNodeStack.removeLast() 32 | } 33 | 34 | node.element = self 35 | 36 | if let parentEnvironmentValues = parent?.environmentValues { 37 | node.environmentValues.merge(parentEnvironmentValues) 38 | } 39 | 40 | observeObjects(node) 41 | restoreStateProperties(node) 42 | 43 | if let bodylessElement = self as? any BodylessElement { 44 | try bodylessElement._expandNode(node, context: context.deeper()) 45 | } 46 | 47 | let shouldRunBody = node.needsRebuild || !equalToPrevious(node) 48 | if !shouldRunBody { 49 | for child in node.children { 50 | try child.rebuildIfNeeded() 51 | } 52 | return 53 | } 54 | 55 | if Body.self != Never.self { 56 | if node.children.isEmpty { 57 | let children = [graph.makeNode()] 58 | node.children = children 59 | children.forEach { child in 60 | child.parent = node 61 | } 62 | } 63 | try body.expandNode(node.children[0], context: context.deeper()) 64 | } 65 | 66 | storeStateProperties(node) 67 | node.previousElement = self 68 | node.needsRebuild = false 69 | } 70 | 71 | private func equalToPrevious(_ node: Node) -> Bool { 72 | guard let previous = node.previousElement as? Self else { 73 | return false 74 | } 75 | let lhs = Mirror(reflecting: self).children 76 | let rhs = Mirror(reflecting: previous).children 77 | return zip(lhs, rhs).allSatisfy { lhs, rhs in 78 | guard lhs.label == rhs.label else { 79 | return false 80 | } 81 | if lhs.value is StateProperty { 82 | return true 83 | } 84 | if !isEqual(lhs.value, rhs.value) { 85 | return false 86 | } 87 | return true 88 | } 89 | } 90 | 91 | private func observeObjects(_ node: Node) { 92 | let mirror = Mirror(reflecting: self) 93 | for child in mirror.children { 94 | guard let observedObject = child.value as? AnyObservedObject else { 95 | return 96 | } 97 | observedObject.addDependency(node) 98 | } 99 | } 100 | 101 | private func restoreStateProperties(_ node: Node) { 102 | let mirror = Mirror(reflecting: self) 103 | for (label, value) in mirror.children { 104 | guard let prop = value as? StateProperty else { continue } 105 | guard let label else { 106 | preconditionFailure("No label for state property.") 107 | } 108 | guard let propValue = node.stateProperties[label] else { continue } 109 | prop.erasedValue = propValue 110 | } 111 | } 112 | 113 | private func storeStateProperties(_ node: Node) { 114 | let m = Mirror(reflecting: self) 115 | for (label, value) in m.children { 116 | guard let prop = value as? StateProperty else { continue } 117 | guard let label else { 118 | preconditionFailure("No label for state property.") 119 | } 120 | node.stateProperties[label] = prop.erasedValue 121 | } 122 | } 123 | } 124 | 125 | public extension Node { 126 | var ancestors: [Node] { 127 | var ancestors: [Node] = [] 128 | var current: Node = self 129 | while let parent = current.parent { 130 | ancestors.append(parent) 131 | current = parent 132 | } 133 | return ancestors 134 | } 135 | 136 | @MainActor 137 | var path: String { 138 | let ancestors = self.ancestors 139 | let path = ancestors.map(\.debugName).joined(separator: ".") 140 | return path + ".\(debugName)" 141 | } 142 | 143 | @MainActor 144 | var name: String { 145 | if let element { 146 | return "\(element.debugName)" 147 | } 148 | return "" 149 | } 150 | 151 | @MainActor 152 | var debugName: String { 153 | if let name = self.debugLabel { 154 | return name 155 | } 156 | if let element { 157 | return "\(element.debugName)" 158 | } 159 | return "" 160 | } 161 | } 162 | 163 | internal extension Element { 164 | var debugName: String { 165 | abbreviatedTypeName(of: self) 166 | } 167 | } 168 | 169 | internal func abbreviatedTypeName(of t: T) -> String { 170 | let name = "\(type(of: t))" 171 | return String(name[..<(name.firstIndex(of: "<") ?? name.endIndex)]) 172 | } 173 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/ElementBuilder.swift: -------------------------------------------------------------------------------- 1 | @resultBuilder 2 | @MainActor 3 | // swiftlint:disable:next convenience_type 4 | public struct ElementBuilder { 5 | public static func buildBlock(_ content: V) -> V { 6 | content 7 | } 8 | 9 | public static func buildBlock() -> EmptyElement { 10 | EmptyElement() 11 | } 12 | 13 | public static func buildBlock(_ content: repeat each Content) -> TupleElement where repeat each Content: Element { 14 | TupleElement(repeat each content) 15 | } 16 | 17 | public static func buildOptional(_ content: Content?) -> some Element where Content: Element { 18 | content 19 | } 20 | 21 | // TODO: #108 #47 Flesh this out (follow ViewBuilder for more). 22 | // TODO: #145 Add unit tests for `ElementBuilder.buildEither`. 23 | /// Produces content for a conditional statement in a multi-statement closure when the condition is true. 24 | public static func buildEither(first: TrueContent) -> _ConditionalContent where TrueContent: Element, FalseContent: Element { 25 | _ConditionalContent(first: first) 26 | } 27 | 28 | /// Produces content for a conditional statement in a multi-statement closure when the condition is false. 29 | public static func buildEither(second: FalseContent) -> _ConditionalContent where TrueContent: Element, FalseContent: Element { 30 | _ConditionalContent(second: second) 31 | } 32 | 33 | public static func buildLimitedAvailability(_ content: Content) -> AnyElement where Content: Element { 34 | AnyElement(content) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/EmptyElement.swift: -------------------------------------------------------------------------------- 1 | public struct EmptyElement: Element { 2 | public typealias Body = Never 3 | 4 | public init() { 5 | // This line intentionally left blank. 6 | } 7 | } 8 | 9 | extension EmptyElement: BodylessElement { 10 | internal func _expandNode(_ node: Node, context: ExpansionContext) throws { 11 | // This line intentionally left blank. 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/ForEach.swift: -------------------------------------------------------------------------------- 1 | // TODO: #106 this is going to be complex. 2 | // TODO: #107 Compare ids to see if they've changed in expandNode. 3 | 4 | public struct ForEach : Element where Data: RandomAccessCollection, ID: Hashable, Content: Element { 5 | @UVState 6 | var ids: [ID] 7 | 8 | var data: Data 9 | var content: (Data.Element) throws -> Content 10 | } 11 | 12 | public extension ForEach { 13 | init(_ data: Data, @ElementBuilder content: @escaping (Data.Element) throws -> Content) where Data: Collection, Data.Element: Identifiable, Data.Element.ID == ID { 14 | self.ids = data.map(\.id) 15 | self.data = data 16 | self.content = content 17 | } 18 | 19 | init(_ data: Data, id: KeyPath, @ElementBuilder content: @escaping (Data.Element) throws -> Content) where Data: Collection { 20 | self.ids = data.map { $0[keyPath: id] } 21 | self.data = data 22 | self.content = content 23 | } 24 | } 25 | 26 | extension ForEach: BodylessElement { 27 | internal func _expandNode(_ node: Node, context: ExpansionContext) throws { 28 | let graph = try node.graph.orThrow(.noCurrentGraph) 29 | var index = 0 30 | 31 | for datum in data { 32 | if node.children.count <= index { 33 | node.children.append(graph.makeNode()) 34 | } 35 | 36 | let content = try content(datum) 37 | try content.expandNode(node.children[index], context: context.deeper()) 38 | index += 1 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/Graph.swift: -------------------------------------------------------------------------------- 1 | internal import os 2 | import UltraviolenceSupport 3 | 4 | // TODO: #24 Make Internal 5 | public class Graph { 6 | internal var activeNodeStack: [Node] = [] 7 | public private(set) var root: Node 8 | internal var signpostID = signposter?.makeSignpostID() 9 | 10 | @MainActor 11 | public init(content: Content) throws where Content: Element { 12 | root = Node() 13 | root.graph = self 14 | root.element = content 15 | } 16 | 17 | @MainActor 18 | public func update(content: Content) throws where Content: Element { 19 | try withIntervalSignpost(signposter, name: "Graph.updateContent()", id: signpostID) { 20 | // TODO: #25 We need to somehow detect if the content has changed. 21 | let saved = Self.current 22 | Self.current = self 23 | defer { 24 | Self.current = saved 25 | } 26 | try content.expandNode(root, context: .init()) 27 | } 28 | } 29 | 30 | @MainActor 31 | // TODO: #149 `rebuildIfNeeded` is no longer being called. Which is worrying. 32 | internal func rebuildIfNeeded() throws { 33 | let saved = Self.current 34 | Self.current = self 35 | defer { 36 | Self.current = saved 37 | } 38 | guard let rootElement = root.element else { 39 | preconditionFailure("Root element is missing.") 40 | } 41 | try rootElement.expandNode(root, context: .init()) 42 | } 43 | 44 | private static let _current = OSAllocatedUnfairLock(uncheckedState: nil) 45 | 46 | internal static var current: Graph? { 47 | get { 48 | _current.withLockUnchecked { $0 } 49 | } 50 | set { 51 | _current.withLockUnchecked { $0 = newValue } 52 | } 53 | } 54 | 55 | internal func makeNode() -> Node { 56 | Node(graph: self) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/Group.swift: -------------------------------------------------------------------------------- 1 | // TODO: #48 Name overlaps with SwiftUI.Group. Consider renaming. In practice this doesn't seem to be causing problems for me. 2 | // NOTE: This is really just a "container" element. 3 | public struct Group : Element where Content: Element { 4 | public typealias Body = Never 5 | 6 | internal let content: Content 7 | 8 | public init(@ElementBuilder content: () throws -> Content) throws { 9 | self.content = try content() 10 | } 11 | } 12 | 13 | extension Group: BodylessElement, BodylessContentElement { 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/Node.swift: -------------------------------------------------------------------------------- 1 | public final class Node { 2 | weak var graph: Graph? 3 | weak var parent: Node? 4 | public internal(set) var children: [Node] = [] 5 | var needsRebuild = true 6 | public internal(set) var element: (any Element)? 7 | var previousElement: (any Element)? 8 | public internal(set) var stateProperties: [String: Any] = [:] 9 | var environmentValues = UVEnvironmentValues() 10 | public internal(set) var debugLabel: String? 11 | 12 | init() { 13 | // This line intentionally left blank. 14 | } 15 | 16 | init(graph: Graph?) { 17 | assert(graph != nil) 18 | self.graph = graph 19 | } 20 | 21 | @MainActor 22 | func rebuildIfNeeded() throws { 23 | try element?.expandNode(self, context: .init()) 24 | } 25 | 26 | func setNeedsRebuild() { 27 | needsRebuild = true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/Optional+Element.swift: -------------------------------------------------------------------------------- 1 | extension Optional: Element, BodylessElement where Wrapped: Element { 2 | public typealias Body = Never 3 | 4 | internal func _expandNode(_ node: Node, context: ExpansionContext) throws { 5 | try self?.expandNode(node, context: context.deeper()) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/StateBox.swift: -------------------------------------------------------------------------------- 1 | internal final class StateBox { 2 | private var _value: Wrapped 3 | private weak var _graph: Graph? 4 | private var dependencies: [WeakBox] = [] 5 | 6 | private var graph: Graph? { 7 | if _graph == nil { 8 | _graph = Graph.current 9 | assert(_graph != nil, "StateBox must be used within a Graph.") 10 | } 11 | return _graph 12 | } 13 | 14 | internal var wrappedValue: Wrapped { 15 | get { 16 | // Remove dependnecies whose values have been deallocated 17 | dependencies = dependencies.filter { $0.wrappedValue != nil } 18 | 19 | // Add current node accessoring the value to list of dependencies 20 | let currentNode = graph?.activeNodeStack.last 21 | if let currentNode, !dependencies.contains(where: { $0() === currentNode }) { 22 | dependencies.append(WeakBox(currentNode)) 23 | } 24 | return _value 25 | } 26 | set { 27 | _value = newValue 28 | valueDidChange() 29 | } 30 | } 31 | 32 | internal var binding: UVBinding = UVBinding( 33 | get: { preconditionFailure("Empty Binding: get() called.") }, 34 | set: { _ in preconditionFailure("Empty Binding: set() called.") } 35 | ) 36 | 37 | internal init(_ wrappedValue: Wrapped) { 38 | self._value = wrappedValue 39 | // swiftlint:disable:next unowned_variable_capture 40 | self.binding = UVBinding(get: { [unowned self] in 41 | self.wrappedValue 42 | // swiftlint:disable:next unowned_variable_capture 43 | }, set: { [unowned self] newValue in 44 | self.wrappedValue = newValue 45 | }) 46 | } 47 | 48 | /// Update dependencies when the value changes 49 | private func valueDidChange() { 50 | dependencies.forEach { $0()?.setNeedsRebuild() } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/TupleElement.swift: -------------------------------------------------------------------------------- 1 | public struct TupleElement : Element { 2 | public typealias Body = Never 3 | 4 | private let children: (repeat each T) 5 | 6 | public init(_ children: repeat each T) { 7 | self.children = (repeat each children) 8 | } 9 | } 10 | 11 | extension TupleElement: BodylessElement { 12 | internal func _expandNode(_ node: Node, context: ExpansionContext) throws { 13 | let graph = try node.graph.orThrow(.noCurrentGraph) 14 | var index = 0 15 | for child in repeat (each children) { 16 | if node.children.count <= index { 17 | node.children.append(graph.makeNode()) 18 | } 19 | try child.expandNode(node.children[index], context: context.deeper()) 20 | index += 1 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/UVBinding.swift: -------------------------------------------------------------------------------- 1 | internal import Foundation 2 | 3 | @propertyWrapper 4 | public struct UVBinding: Equatable { 5 | private let get: () -> Value 6 | private let set: (Value) -> Void 7 | // TOOD: Use a less expensive unique identifier 8 | private let id = UUID() 9 | 10 | public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) { 11 | self.get = get 12 | self.set = set 13 | } 14 | 15 | public var wrappedValue: Value { 16 | get { get() } 17 | nonmutating set { set(newValue) } 18 | } 19 | 20 | public static func == (lhs: Self, rhs: Self) -> Bool { 21 | lhs.id == rhs.id 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/UVEnvironmentValues.swift: -------------------------------------------------------------------------------- 1 | public struct UVEnvironmentValues { 2 | struct Key: Hashable, CustomDebugStringConvertible { 3 | var id: ObjectIdentifier // TODO: #49 We don't need to store this. But AnyIdentifgier gets a tad upset. 4 | var value: Any.Type 5 | } 6 | 7 | class Storage { 8 | weak var parent: Storage? 9 | var values: [Key: Any] = [:] 10 | } 11 | 12 | var storage = Storage() 13 | 14 | internal mutating func merge(_ parent: Self) { 15 | precondition(parent.storage !== self.storage) 16 | storage.parent = parent.storage 17 | } 18 | } 19 | 20 | public protocol UVEnvironmentKey { 21 | associatedtype Value 22 | static var defaultValue: Value { get } 23 | } 24 | 25 | public extension UVEnvironmentValues { 26 | subscript(key: Key.Type) -> Key.Value { 27 | get { 28 | if let value = storage.get(.init(key)) as? Key.Value { 29 | return value 30 | } 31 | return Key.defaultValue 32 | } 33 | set { 34 | // TODO: #26 Use isKnownUniquelyReferenced. 35 | storage.values[.init(key)] = newValue 36 | } 37 | } 38 | } 39 | 40 | // TODO: #30 Make into actual modifier. 41 | internal struct EnvironmentWritingModifier: Element, BodylessElement { 42 | var content: Content 43 | var modify: (inout UVEnvironmentValues) -> Void 44 | 45 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 46 | modify(&node.environmentValues) 47 | try content.expandNode(node, context: context.deeper()) 48 | } 49 | } 50 | 51 | public extension Element { 52 | func environment(_ keyPath: WritableKeyPath, _ value: Value) -> some Element { 53 | EnvironmentWritingModifier(content: self) { environmentValues in 54 | environmentValues[keyPath: keyPath] = value 55 | } 56 | } 57 | } 58 | 59 | // MARK: - 60 | 61 | public struct EnvironmentReader: Element, BodylessElement { 62 | var keyPath: KeyPath 63 | var content: (Value) throws -> Content 64 | 65 | public init(keyPath: KeyPath, @ElementBuilder content: @escaping (Value) throws -> Content) { 66 | self.keyPath = keyPath 67 | self.content = content 68 | } 69 | 70 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 71 | let value = node.environmentValues[keyPath: keyPath] 72 | try content(value).expandNode(node, context: context.deeper()) 73 | } 74 | } 75 | 76 | // TODO: #50 SwiftUI.Environment adopts DynamicProperty. 77 | // public protocol DynamicProperty { 78 | // mutating func update() 79 | // } 80 | // 81 | // extension DynamicProperty { 82 | // public mutating func update() 83 | // } 84 | 85 | // MARK: - 86 | 87 | @propertyWrapper 88 | public struct UVEnvironment { 89 | public var wrappedValue: Value { 90 | guard let graph = Graph.current else { 91 | preconditionFailure("Environment must be used within a Graph.") 92 | } 93 | guard let currentNode = graph.activeNodeStack.last else { 94 | preconditionFailure("Environment must be used within a Node.") 95 | } 96 | return currentNode.environmentValues[keyPath: keyPath] 97 | } 98 | 99 | private var keyPath: KeyPath 100 | 101 | public init(_ keyPath: KeyPath) { 102 | self.keyPath = keyPath 103 | } 104 | } 105 | 106 | // MARK: - 107 | 108 | extension UVEnvironmentValues.Key { 109 | static func == (lhs: Self, rhs: Self) -> Bool { 110 | lhs.id == rhs.id 111 | } 112 | 113 | func hash(into hasher: inout Hasher) { 114 | id.hash(into: &hasher) 115 | } 116 | 117 | init(_ key: K.Type) { 118 | id = ObjectIdentifier(key) 119 | value = key 120 | } 121 | 122 | var debugDescription: String { 123 | "\(value)" 124 | } 125 | } 126 | 127 | extension UVEnvironmentValues.Storage { 128 | // TODO: #115 Replace with subscript. 129 | func get(_ key: UVEnvironmentValues.Key) -> Any? { 130 | if let value = values[key] { 131 | return value 132 | } 133 | // TODO: #76 This can infinite loop if there is a cycle. *WHY* is there a cycle. 134 | if let parent, let value = parent.get(key) { 135 | return value 136 | } 137 | return nil 138 | } 139 | } 140 | 141 | extension UVEnvironmentValues.Storage: CustomDebugStringConvertible { 142 | public var debugDescription: String { 143 | let keys = values.map { "\($0.key)".trimmingPrefix("__Key_") }.sorted() 144 | return "([\(keys.joined(separator: ", "))], parent: \(parent != nil)))" 145 | } 146 | } 147 | 148 | extension UVEnvironmentValues: CustomDebugStringConvertible { 149 | public var debugDescription: String { 150 | "(storage: \(storage.debugDescription))" 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/UVObservedObject.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | internal protocol AnyObservedObject { 4 | @MainActor 5 | func addDependency(_ node: Node) 6 | } 7 | 8 | // MARK: - 9 | 10 | @propertyWrapper 11 | public struct UVObservedObject { 12 | @ObservedObjectBox 13 | private var object: ObjectType 14 | 15 | public init(wrappedValue: ObjectType) { 16 | _object = ObservedObjectBox(wrappedValue) 17 | } 18 | 19 | public var wrappedValue: ObjectType { 20 | object 21 | } 22 | 23 | public var projectedValue: ProjectedValue { 24 | .init(self) 25 | } 26 | } 27 | 28 | extension UVObservedObject: Equatable { 29 | public static func == (l: UVObservedObject, r: UVObservedObject) -> Bool { 30 | l.wrappedValue === r.wrappedValue 31 | } 32 | } 33 | 34 | extension UVObservedObject: AnyObservedObject { 35 | internal func addDependency(_ node: Node) { 36 | _object.addDependency(node) 37 | } 38 | } 39 | 40 | // MARK: - 41 | 42 | @propertyWrapper 43 | private final class ObservedObjectBox { 44 | let wrappedValue: Wrapped 45 | var cancellable: AnyCancellable? 46 | weak var node: Node? 47 | 48 | init(_ wrappedValue: Wrapped) { 49 | self.wrappedValue = wrappedValue 50 | } 51 | 52 | @MainActor 53 | func addDependency(_ node: Node) { 54 | guard node !== self.node else { 55 | return 56 | } 57 | self.node = node 58 | cancellable = wrappedValue.objectWillChange.sink { _ in 59 | node.setNeedsRebuild() 60 | } 61 | } 62 | } 63 | 64 | // MARK: - 65 | 66 | @dynamicMemberLookup 67 | public struct ProjectedValue { 68 | private var observedObject: UVObservedObject 69 | 70 | internal init(_ observedObject: UVObservedObject) { 71 | self.observedObject = observedObject 72 | } 73 | 74 | public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> UVBinding { 75 | UVBinding(get: { 76 | observedObject.wrappedValue[keyPath: keyPath] 77 | }, set: { newValue in 78 | observedObject.wrappedValue[keyPath: keyPath] = newValue 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Core/UVState.swift: -------------------------------------------------------------------------------- 1 | internal protocol StateProperty { 2 | var erasedValue: Any { get nonmutating set } 3 | } 4 | 5 | @propertyWrapper 6 | public struct UVState { 7 | @Box 8 | private var state: StateBox 9 | 10 | public init(wrappedValue: Value) { 11 | _state = Box(StateBox(wrappedValue)) 12 | } 13 | 14 | public var wrappedValue: Value { 15 | get { state.wrappedValue } 16 | nonmutating set { state.wrappedValue = newValue } 17 | } 18 | 19 | public var projectedValue: UVBinding { 20 | state.binding 21 | } 22 | } 23 | 24 | extension UVState: StateProperty { 25 | internal var erasedValue: Any { 26 | get { state } 27 | nonmutating set { 28 | guard let newValue = newValue as? StateBox else { 29 | preconditionFailure("Expected StateBox in State.value set") 30 | } 31 | state = newValue 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/DebugLabelModifier.swift: -------------------------------------------------------------------------------- 1 | internal struct DebugLabelModifier : Element, BodylessElement, BodylessContentElement where Content: Element { 2 | let debugLabel: String 3 | let content: Content 4 | 5 | init(_ debugLabel: String, content: Content) { 6 | self.debugLabel = debugLabel 7 | self.content = content 8 | } 9 | 10 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 11 | // TODO: #95 We don't expand a new node - instead of we create expand our child's node. This is subtle and confusing and we really need to clean up all this: 12 | // self._expandNode() vs content.expandNode vs ....... 13 | // a lot more nodes COULD work this way. 14 | node.debugLabel = debugLabel 15 | try content.expandNode(node, context: context.deeper()) 16 | } 17 | } 18 | 19 | public extension Element { 20 | func debugLabel(_ debugLabel: String) -> some Element { 21 | DebugLabelModifier(debugLabel, content: self) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Draw.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | 3 | // TODO: #100 this is no different than a EncivonmentReader 4 | public struct Draw: Element, BodylessElement { 5 | public typealias Body = Never 6 | 7 | var encodeGeometry: (MTLRenderCommandEncoder) throws -> Void 8 | 9 | public init(encodeGeometry: @escaping (MTLRenderCommandEncoder) throws -> Void) { 10 | self.encodeGeometry = encodeGeometry 11 | } 12 | 13 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 14 | // This line intentionally left blank. 15 | } 16 | 17 | func workloadEnter(_ node: Node) throws { 18 | let renderCommandEncoder = try node.environmentValues.renderCommandEncoder.orThrow(.missingEnvironment(\.renderCommandEncoder)) 19 | try encodeGeometry(renderCommandEncoder) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Graph+Process.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Metal 3 | import UltraviolenceSupport 4 | 5 | public extension Graph { 6 | @MainActor 7 | func processSetup() throws { 8 | try withIntervalSignpost(signposter, name: "Graph.processSetup()") { 9 | try process { element, node in 10 | try element.setupEnter(node) 11 | } exit: { element, node in 12 | try element.setupExit(node) 13 | } 14 | } 15 | } 16 | 17 | @MainActor 18 | func processWorkload() throws { 19 | try withIntervalSignpost(signposter, name: "Graph.processWorkload()") { 20 | try process { element, node in 21 | try element.workloadEnter(node) 22 | } exit: { element, node in 23 | try element.workloadExit(node) 24 | } 25 | } 26 | } 27 | } 28 | 29 | internal extension Graph { 30 | @MainActor 31 | func process(enter: (any BodylessElement, Node) throws -> Void, exit: (any BodylessElement, Node) throws -> Void) throws { 32 | try visit { node in 33 | if let body = node.element as? any BodylessElement { 34 | try enter(body, node) 35 | } 36 | } 37 | exit: { node in 38 | if let body = node.element as? any BodylessElement { 39 | try exit(body, node) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/LoggingElement.swift: -------------------------------------------------------------------------------- 1 | public struct LoggingElement: Element, BodylessElement { 2 | public init() { 3 | // This line intentionally left blank. 4 | } 5 | 6 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 7 | // This line intentionally left blank. 8 | } 9 | 10 | func setupEnter(_ node: Node) throws { 11 | logger?.log("setupEnter") 12 | } 13 | 14 | func setupExit(_ node: Node) throws { 15 | logger?.log("setupExit") 16 | } 17 | 18 | func workloadEnter(_ node: Node) throws { 19 | logger?.log("workloadEnter") 20 | } 21 | 22 | func workloadExit(_ node: Node) throws { 23 | logger?.log("workloadExit") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/MetalFXSpatial.swift: -------------------------------------------------------------------------------- 1 | #if canImport(MetalFX) 2 | import Metal 3 | import MetalFX 4 | import UltraviolenceSupport 5 | 6 | public struct MetalFXSpatial: Element { 7 | @UVState 8 | var scaler: MTLFXSpatialScaler? 9 | 10 | var inputTexture: MTLTexture 11 | var outputTexture: MTLTexture 12 | 13 | @UVEnvironment(\.commandBuffer) 14 | var commandBuffer 15 | 16 | public init(inputTexture: MTLTexture, outputTexture: MTLTexture) { 17 | self.inputTexture = inputTexture 18 | self.outputTexture = outputTexture 19 | } 20 | 21 | public var body: some Element { 22 | AnyBodylessElement() 23 | .onSetupEnter { 24 | scaler = try makeScaler() 25 | } 26 | .onWorkloadEnter { 27 | var scaler = try scaler.orThrow(.undefined) 28 | // TODO: #55, #70 Instead of doing this we need to have some kind of "onChange" and merely mark "setupNeeded" 29 | if scaler.outputWidth != outputTexture.width || scaler.outputHeight != outputTexture.height { 30 | scaler = try makeScaler() 31 | self.scaler = scaler 32 | } 33 | let commandBuffer = try commandBuffer.orThrow(.missingEnvironment(\.commandBuffer)) 34 | scaler.colorTexture = inputTexture 35 | scaler.inputContentWidth = inputTexture.width 36 | scaler.inputContentHeight = inputTexture.height 37 | scaler.outputTexture = outputTexture 38 | scaler.encode(commandBuffer: commandBuffer) 39 | } 40 | } 41 | 42 | func makeScaler() throws -> MTLFXSpatialScaler { 43 | let descriptor = MTLFXSpatialScalerDescriptor() 44 | descriptor.colorTextureFormat = inputTexture.pixelFormat 45 | descriptor.outputTextureFormat = outputTexture.pixelFormat 46 | descriptor.inputWidth = inputTexture.width 47 | descriptor.inputHeight = inputTexture.height 48 | descriptor.outputWidth = outputTexture.width 49 | descriptor.outputHeight = outputTexture.height 50 | let device = _MTLCreateSystemDefaultDevice() 51 | return try descriptor.makeSpatialScaler(device: device).orThrow(.undefined) 52 | } 53 | } 54 | #endif 55 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Modifier.swift: -------------------------------------------------------------------------------- 1 | // TODO: #30 Stuff like @Environment/@State on modifiers won't work because. 2 | 3 | @available(*, deprecated, message: "Incomplete: See issue https://github.com/schwa/ultraviolence/issues/30") 4 | public protocol ElementModifier { 5 | typealias Content = AnyElement 6 | associatedtype Body: Element 7 | 8 | @MainActor 9 | func body(content: Content) -> Body 10 | } 11 | 12 | internal struct ModifiedContent : Element where Content: Element, Modifier: ElementModifier { 13 | private let content: Content 14 | private let modifier: Modifier 15 | 16 | internal init(content: Content, modifier: Modifier) { 17 | self.content = content 18 | self.modifier = modifier 19 | } 20 | 21 | internal var body: some Element { 22 | modifier.body(content: AnyElement(content)) 23 | } 24 | } 25 | 26 | @available(*, deprecated, message: "Incomplete: See issue https://github.com/schwa/ultraviolence/issues/30") 27 | public extension Element { 28 | func modifier(_ modifier: Modifier) -> some Element where Modifier: ElementModifier { 29 | ModifiedContent(content: self, modifier: modifier) 30 | } 31 | } 32 | 33 | // MARK: - 34 | 35 | @available(*, deprecated, message: "Incomplete: See issue https://github.com/schwa/ultraviolence/issues/30") 36 | public struct PassthroughModifier: ElementModifier { 37 | public init() { 38 | // This line intentionally left blank. 39 | } 40 | 41 | @MainActor 42 | public func body(content: Content) -> some Element { 43 | content 44 | } 45 | } 46 | 47 | // TODO: #96 Type system is not letting something as simple as this. 48 | // public struct AnyModifier: ElementModifier { 49 | // private let modify: (Content) -> Body 50 | // 51 | // @MainActor 52 | // public func body(content: Content) -> Body { 53 | // modify(content) 54 | // } 55 | // } 56 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/ParameterValue.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | 3 | internal enum ParameterValue { 4 | case texture(MTLTexture?) 5 | case samplerState(MTLSamplerState?) 6 | case buffer(MTLBuffer?, Int) 7 | case array([T]) 8 | case value(T) 9 | } 10 | 11 | extension ParameterValue: CustomDebugStringConvertible { 12 | var debugDescription: String { 13 | switch self { 14 | case .texture(let texture): 15 | return "Texture" 16 | case .samplerState(let samplerState): 17 | return "SamplerState" 18 | case .buffer(let buffer, let offset): 19 | return "Buffer(\(String(describing: buffer?.label)), offset: \(offset)" 20 | case .array(let array): 21 | return "Array" 22 | case .value(let value): 23 | return "\(value)" 24 | } 25 | } 26 | } 27 | 28 | // TODO: #21 We really need to rethink type safety of ParameterValue. Make this a struct and keep internal enum - still need to worry about though. 29 | // extension ParameterValue where T == () { 30 | // static func texture(_ texture: MTLTexture) -> ParameterValue { 31 | // .texture(texture) // Error. Ambiguous use of 'texture'. 32 | // } 33 | // } 34 | 35 | internal extension MTLRenderCommandEncoder { 36 | func setValue(_ value: ParameterValue, index: Int, functionType: MTLFunctionType) { 37 | switch value { 38 | case .texture(let texture): 39 | setTexture(texture, index: index, functionType: functionType) 40 | 41 | case .samplerState(let samplerState): 42 | setSamplerState(samplerState, index: index, functionType: functionType) 43 | 44 | case .buffer(let buffer, let offset): 45 | setBuffer(buffer, offset: offset, index: index, functionType: functionType) 46 | 47 | case .array(let array): 48 | setUnsafeBytes(of: array, index: index, functionType: functionType) 49 | 50 | case .value(let value): 51 | setUnsafeBytes(of: value, index: index, functionType: functionType) 52 | } 53 | } 54 | } 55 | 56 | internal extension MTLComputeCommandEncoder { 57 | func setValue(_ value: ParameterValue, index: Int) { 58 | switch value { 59 | case .texture(let texture): 60 | setTexture(texture, index: index) 61 | 62 | case .samplerState(let samplerState): 63 | setSamplerState(samplerState, index: index) 64 | 65 | case .buffer(let buffer, let offset): 66 | setBuffer(buffer, offset: offset, index: index) 67 | 68 | case .array(let array): 69 | setUnsafeBytes(of: array, index: index) 70 | 71 | case .value(let value): 72 | setUnsafeBytes(of: value, index: index) 73 | } 74 | } 75 | } 76 | 77 | // MARK: - 78 | 79 | internal struct AnyParameterValue { 80 | var renderSetValue: (MTLRenderCommandEncoder, Int, MTLFunctionType) -> Void 81 | var computeSetValue: (MTLComputeCommandEncoder, Int) -> Void 82 | var _debugDescription: () -> String 83 | 84 | init(_ value: ParameterValue) { 85 | self.renderSetValue = { encoder, index, functionType in 86 | encoder.setValue(value, index: index, functionType: functionType) 87 | } 88 | self.computeSetValue = { encoder, index in 89 | encoder.setValue(value, index: index) 90 | } 91 | self._debugDescription = { 92 | value.debugDescription 93 | } 94 | } 95 | } 96 | 97 | extension AnyParameterValue: CustomDebugStringConvertible { 98 | var debugDescription: String { 99 | _debugDescription() 100 | } 101 | } 102 | 103 | internal extension MTLRenderCommandEncoder { 104 | func setValue(_ value: AnyParameterValue, index: Int, functionType: MTLFunctionType) { 105 | value.renderSetValue(self, index, functionType) 106 | } 107 | } 108 | 109 | internal extension MTLComputeCommandEncoder { 110 | func setValue(_ value: AnyParameterValue, index: Int) { 111 | value.computeSetValue(self, index) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Parameters.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Metal 3 | import simd 4 | import UltraviolenceSupport 5 | 6 | // TODO: #62 instead of being typed we need an "AnyParameter" and this needs to take a dictionary of AnyParameters 7 | // TODO: #123 Rename it to be a modifier? 8 | internal struct ParameterElement: Element, BodylessElement, BodylessContentElement where Content: Element { 9 | var parameters: [String: Parameter] 10 | var content: Content 11 | 12 | internal init(functionType: MTLFunctionType? = nil, name: String, value: ParameterValue, content: Content) { 13 | self.parameters = [name: .init(name: name, functionType: functionType, value: value)] 14 | self.content = content 15 | } 16 | 17 | func workloadEnter(_ node: Node) throws { 18 | let reflection = try node.environmentValues.reflection.orThrow(.missingEnvironment(\.reflection)) 19 | let renderCommandEncoder = node.environmentValues.renderCommandEncoder 20 | let computeCommandEncoder = node.environmentValues.computeCommandEncoder 21 | for parameter in parameters.values { 22 | switch (renderCommandEncoder, computeCommandEncoder) { 23 | case (.some(let renderCommandEncoder), nil): 24 | try parameter.set(on: renderCommandEncoder, reflection: reflection) 25 | case (nil, .some(let computeCommandEncoder)): 26 | try parameter.set(on: computeCommandEncoder, reflection: reflection) 27 | case (.some, .some): 28 | preconditionFailure("Trying to process \(self) with both a render command encoder and a compute command encoder.") 29 | default: 30 | preconditionFailure("Trying to process `\(self) without a command encoder.") 31 | } 32 | } 33 | } 34 | } 35 | 36 | // MARK: - 37 | 38 | internal struct Parameter { 39 | var name: String 40 | var functionType: MTLFunctionType? 41 | var value: AnyParameterValue 42 | 43 | init(name: String, functionType: MTLFunctionType? = nil, value: ParameterValue) { 44 | self.name = name 45 | self.functionType = functionType 46 | self.value = AnyParameterValue(value) 47 | } 48 | 49 | @MainActor 50 | func set(on encoder: MTLRenderCommandEncoder, reflection: Reflection) throws { 51 | try encoder.withDebugGroup("MTLRenderCommandEncoder(\(encoder.label.quoted)): \(name.quoted) = \(value)") { 52 | switch functionType { 53 | case .vertex: 54 | if let index = reflection.binding(forType: .vertex, name: name) { 55 | encoder.setValue(value, index: index, functionType: .vertex) 56 | } 57 | case .fragment: 58 | if let index = reflection.binding(forType: .fragment, name: name) { 59 | encoder.setValue(value, index: index, functionType: .fragment) 60 | } 61 | case nil: 62 | let vertexIndex = reflection.binding(forType: .vertex, name: name) 63 | let fragmentIndex = reflection.binding(forType: .fragment, name: name) 64 | switch (vertexIndex, fragmentIndex) { 65 | case (.some(let vertexIndex), .some(let fragmentIndex)): 66 | preconditionFailure("Ambiguous parameter, found parameter named \(name) in both vertex (index: #\(vertexIndex)) and fragment (index: #\(fragmentIndex)) shaders.") 67 | case (.some(let vertexIndex), .none): 68 | encoder.setValue(value, index: vertexIndex, functionType: .vertex) 69 | case (.none, .some(let fragmentIndex)): 70 | encoder.setValue(value, index: fragmentIndex, functionType: .fragment) 71 | case (.none, .none): 72 | logger?.info("Parameter \(name) not found in reflection \(reflection.debugDescription).") 73 | throw UltraviolenceError.missingBinding(name) 74 | } 75 | default: 76 | fatalError("Invalid shader type \(functionType.debugDescription).") 77 | } 78 | } 79 | } 80 | 81 | func set(on encoder: MTLComputeCommandEncoder, reflection: Reflection) throws { 82 | guard functionType == .kernel || functionType == nil else { 83 | throw UltraviolenceError.generic("Invalid function type \(functionType.debugDescription).") 84 | } 85 | let index = try reflection.binding(forType: .kernel, name: name).orThrow(.missingBinding(name)) 86 | encoder.setValue(value, index: index) 87 | } 88 | } 89 | 90 | // MARK: - 91 | 92 | public extension Element { 93 | // TODO: #124 Move functionType to front of the parameter list 94 | func parameter(_ name: String, _ value: SIMD4, functionType: MTLFunctionType? = nil) -> some Element { 95 | ParameterElement(functionType: functionType, name: name, value: .value(value), content: self) 96 | } 97 | 98 | func parameter(_ name: String, _ value: simd_float4x4, functionType: MTLFunctionType? = nil) -> some Element { 99 | ParameterElement(functionType: functionType, name: name, value: .value(value), content: self) 100 | } 101 | 102 | func parameter(_ name: String, texture: MTLTexture?, functionType: MTLFunctionType? = nil) -> some Element { 103 | ParameterElement(functionType: functionType, name: name, value: ParameterValue<()>.texture(texture), content: self) 104 | } 105 | 106 | func parameter(_ name: String, samplerState: MTLSamplerState, functionType: MTLFunctionType? = nil) -> some Element { 107 | ParameterElement(functionType: functionType, name: name, value: ParameterValue<()>.samplerState(samplerState), content: self) 108 | } 109 | 110 | func parameter(_ name: String, buffer: MTLBuffer, offset: Int = 0, functionType: MTLFunctionType? = nil) -> some Element { 111 | ParameterElement(functionType: functionType, name: name, value: ParameterValue<()>.buffer(buffer, offset), content: self) 112 | } 113 | 114 | func parameter(_ name: String, values: [some Any], functionType: MTLFunctionType? = nil) -> some Element { 115 | assert(isPODArray(values), "Parameter values must be a POD type.") 116 | return ParameterElement(functionType: functionType, name: name, value: .array(values), content: self) 117 | } 118 | 119 | func parameter(_ name: String, value: some Any, functionType: MTLFunctionType? = nil) -> some Element { 120 | assert(isPOD(value), "Parameter value must be a POD type.") 121 | return ParameterElement(functionType: functionType, name: name, value: .value(value), content: self) 122 | } 123 | } 124 | 125 | extension String { 126 | var quoted: String { 127 | "\"\(self)\"" 128 | } 129 | } 130 | 131 | extension Optional { 132 | var quoted: String { 133 | switch self { 134 | case .none: 135 | return "nil" 136 | case .some(let string): 137 | return string.quoted 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Reflection.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | 3 | public struct Reflection { 4 | public struct Key: Hashable { 5 | public var functionType: MTLFunctionType 6 | public var name: String 7 | } 8 | 9 | private var bindings: [Key: Int] = [:] 10 | 11 | public func binding(forType functionType: MTLFunctionType, name: String) -> Int? { 12 | bindings[.init(functionType: functionType, name: name)] 13 | } 14 | } 15 | 16 | extension Reflection { 17 | init(_ renderPipelineReflection: MTLRenderPipelineReflection) { 18 | for binding in renderPipelineReflection.fragmentBindings { 19 | bindings[.init(functionType: .fragment, name: binding.name)] = binding.index 20 | } 21 | for binding in renderPipelineReflection.vertexBindings { 22 | bindings[.init(functionType: .vertex, name: binding.name)] = binding.index 23 | } 24 | } 25 | } 26 | 27 | extension Reflection { 28 | init(_ computePipelineReflection: MTLComputePipelineReflection) { 29 | for binding in computePipelineReflection.bindings { 30 | bindings[.init(functionType: .kernel, name: binding.name)] = binding.index 31 | } 32 | } 33 | } 34 | 35 | extension Reflection: CustomDebugStringConvertible { 36 | public var debugDescription: String { 37 | bindings.debugDescription 38 | } 39 | } 40 | 41 | extension Reflection.Key: CustomDebugStringConvertible { 42 | public var debugDescription: String { 43 | "Key(type: .\(functionType) name: \"\(name)\")" 44 | } 45 | } 46 | 47 | extension MTLFunctionType: @retroactive CustomDebugStringConvertible { 48 | public var debugDescription: String { 49 | switch self { 50 | case .vertex: 51 | return "vertex" 52 | case .fragment: 53 | return "fragment" 54 | case .kernel: 55 | return "kernel" 56 | case .visible: 57 | return "visible" 58 | case .intersection: 59 | return "intersection" 60 | case .mesh: 61 | return "mesh" 62 | case .object: 63 | return "object" 64 | @unknown default: 65 | return "unknown" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/RenderPass.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | 3 | public struct RenderPass : Element, BodylessElement, BodylessContentElement where Content: Element { 4 | var content: Content 5 | 6 | public init(@ElementBuilder content: () throws -> Content) throws { 7 | self.content = try content() 8 | } 9 | 10 | func setupEnter(_ node: Node) throws { 11 | let renderPipelineDescriptor = MTLRenderPipelineDescriptor() 12 | node.environmentValues.renderPipelineDescriptor = renderPipelineDescriptor 13 | } 14 | 15 | func workloadEnter(_ node: Node) throws { 16 | let commandBuffer = try node.environmentValues.commandBuffer.orThrow(.missingEnvironment(\.commandBuffer)) 17 | let renderPassDescriptor = try node.environmentValues.renderPassDescriptor.orThrow(.missingEnvironment(\.renderPassDescriptor)) 18 | let renderCommandEncoder = try commandBuffer._makeRenderCommandEncoder(descriptor: renderPassDescriptor) 19 | node.environmentValues.renderCommandEncoder = renderCommandEncoder 20 | } 21 | 22 | func workloadExit(_ node: Node) throws { 23 | let renderCommandEncoder = try node.environmentValues.renderCommandEncoder.orThrow(.missingEnvironment(\.renderCommandEncoder)) 24 | renderCommandEncoder.endEncoding() 25 | node.environmentValues.renderCommandEncoder = nil 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/RenderPassDescriptorModifier.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | 3 | // TODO: #30 Make into actual Modifier. 4 | internal struct RenderPassDescriptorModifier: Element where Content: Element { 5 | @UVEnvironment(\.renderPassDescriptor) 6 | var renderPassDescriptor 7 | 8 | var content: Content 9 | var modify: (MTLRenderPassDescriptor) -> Void 10 | 11 | // TODO: #72 this is pretty bad. We're only modifying it for workload NOT setup. And we're modifying it globally - even for elements further up teh stack. 12 | var body: some Element { 13 | get throws { 14 | content.environment(\.renderPassDescriptor, try modifiedRenderPassDescriptor()) 15 | } 16 | } 17 | 18 | func modifiedRenderPassDescriptor() throws -> MTLRenderPassDescriptor { 19 | let renderPassDescriptor = renderPassDescriptor.orFatalError() 20 | let copy = (renderPassDescriptor.copy() as? MTLRenderPassDescriptor).orFatalError() 21 | modify(copy) 22 | return copy 23 | } 24 | } 25 | 26 | public extension Element { 27 | func renderPassDescriptorModifier(_ modify: @escaping (MTLRenderPassDescriptor) -> Void) -> some Element { 28 | RenderPassDescriptorModifier(content: self, modify: modify) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/RenderPipeline.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import UltraviolenceSupport 3 | 4 | public struct RenderPipeline : Element, BodylessElement, BodylessContentElement where Content: Element { 5 | public typealias Body = Never 6 | @UVEnvironment(\.device) 7 | var device 8 | 9 | @UVEnvironment(\.depthStencilState) 10 | var depthStencilState 11 | 12 | var vertexShader: VertexShader 13 | var fragmentShader: FragmentShader 14 | var content: Content 15 | 16 | @UVState 17 | var reflection: Reflection? 18 | 19 | public init(vertexShader: VertexShader, fragmentShader: FragmentShader, @ElementBuilder content: () throws -> Content) throws { 20 | self.vertexShader = vertexShader 21 | self.fragmentShader = fragmentShader 22 | self.content = try content() 23 | } 24 | 25 | func setupEnter(_ node: Node) throws { 26 | let environment = node.environmentValues 27 | 28 | let renderPassDescriptor = try environment.renderPassDescriptor.orThrow(.missingEnvironment(\.renderPassDescriptor)).copyWithType(MTLRenderPassDescriptor.self) 29 | 30 | let renderPipelineDescriptor = try environment.renderPipelineDescriptor.orThrow(.missingEnvironment(\.renderPipelineDescriptor)) 31 | renderPipelineDescriptor.vertexFunction = vertexShader.function 32 | renderPipelineDescriptor.fragmentFunction = fragmentShader.function 33 | 34 | guard let vertexDescriptor = environment.vertexDescriptor ?? vertexShader.vertexDescriptor else { 35 | // TODO: #101 We were falling back to vertexShader.vertexDescriptor but that seems to be unreliable. 36 | throw UltraviolenceError.generic("No vertex descriptor") 37 | } 38 | renderPipelineDescriptor.vertexDescriptor = vertexDescriptor 39 | 40 | // TODO: #102 We don't want to overwrite anything already set. 41 | // TODO: #103 This is copying everything from the render pass descriptor. But really we should be getting this entirely from the enviroment. 42 | if let colorAttachment0Texture = renderPassDescriptor.colorAttachments[0].texture { 43 | renderPipelineDescriptor.colorAttachments[0].pixelFormat = colorAttachment0Texture.pixelFormat 44 | } 45 | if let depthAttachmentTexture = renderPassDescriptor.depthAttachment?.texture { 46 | renderPipelineDescriptor.depthAttachmentPixelFormat = depthAttachmentTexture.pixelFormat 47 | } 48 | if let stencilAttachmentTexture = renderPassDescriptor.stencilAttachment?.texture { 49 | renderPipelineDescriptor.stencilAttachmentPixelFormat = stencilAttachmentTexture.pixelFormat 50 | } 51 | let device = try device.orThrow(.missingEnvironment(\.device)) 52 | let (renderPipelineState, reflection) = try device.makeRenderPipelineState(descriptor: renderPipelineDescriptor, options: .bindingInfo) 53 | self.reflection = .init(reflection.orFatalError(.resourceCreationFailure("Failed to create reflection."))) 54 | 55 | if environment.depthStencilState == nil, let depthStencilDescriptor = environment.depthStencilDescriptor { 56 | let depthStencilState = device.makeDepthStencilState(descriptor: depthStencilDescriptor) 57 | node.environmentValues.depthStencilState = depthStencilState 58 | } 59 | 60 | node.environmentValues.renderPipelineState = renderPipelineState 61 | node.environmentValues.reflection = self.reflection 62 | } 63 | 64 | func workloadEnter(_ node: Node) throws { 65 | let renderCommandEncoder = try node.environmentValues.renderCommandEncoder.orThrow(.missingEnvironment(\.renderCommandEncoder)) 66 | let renderPipelineState = try node.environmentValues.renderPipelineState.orThrow(.missingEnvironment(\.renderPipelineState)) 67 | 68 | if let depthStencilState { 69 | renderCommandEncoder.setDepthStencilState(depthStencilState) 70 | } 71 | 72 | renderCommandEncoder.setRenderPipelineState(renderPipelineState) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/RenderPipelineDescriptorModifier.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | 3 | // TODO: #30 Make into actual Modifier. 4 | // TODO: #99 is this actually necessary? Elements just use an environment? 5 | public struct RenderPipelineDescriptorModifier: Element where Content: Element { 6 | @UVEnvironment(\.renderPipelineDescriptor) 7 | var renderPipelineDescriptor 8 | 9 | var content: Content 10 | var modify: (MTLRenderPipelineDescriptor) -> Void 11 | 12 | // TODO: #72 this is pretty bad. We're only modifying it for workload NOT setup. And we're modifying it globally - even for elements further up teh stack. 13 | public var body: some Element { 14 | get throws { 15 | content.environment(\.renderPipelineDescriptor, try modifiedRenderPipelineDescriptor()) 16 | } 17 | } 18 | 19 | func modifiedRenderPipelineDescriptor() throws -> MTLRenderPipelineDescriptor { 20 | let renderPipelineDescriptor = renderPipelineDescriptor.orFatalError() 21 | let copy = (renderPipelineDescriptor.copy() as? MTLRenderPipelineDescriptor).orFatalError() 22 | modify(copy) 23 | return copy 24 | } 25 | } 26 | 27 | public extension Element { 28 | func renderPipelineDescriptorModifier(_ modify: @escaping (MTLRenderPipelineDescriptor) -> Void) -> some Element { 29 | RenderPipelineDescriptorModifier(content: self, modify: modify) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Roots/Element+Run.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import UltraviolenceSupport 3 | 4 | public extension Element { 5 | @MainActor 6 | func run() throws { 7 | // TODO: #27 This has surprisingly little to do with compute. It's basically the same as offscreen renderer. 8 | let device = _MTLCreateSystemDefaultDevice() 9 | let commandQueue = try device._makeCommandQueue() 10 | 11 | let content = CommandBufferElement(completion: .commitAndWaitUntilCompleted) { 12 | self 13 | } 14 | .environment(\.commandQueue, commandQueue) 15 | .environment(\.device, device) 16 | 17 | let graph = try Graph(content: content) 18 | try graph.processSetup() 19 | try graph.processWorkload() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Roots/OffscreenRenderer.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Metal 3 | import UltraviolenceSupport 4 | 5 | // TODO: #111 Rename. 6 | public struct OffscreenRenderer { 7 | public var device: MTLDevice 8 | public var size: CGSize 9 | public var colorTexture: MTLTexture 10 | public var depthTexture: MTLTexture 11 | public var renderPassDescriptor: MTLRenderPassDescriptor 12 | public var commandQueue: MTLCommandQueue 13 | 14 | public init(size: CGSize, colorTexture: MTLTexture, depthTexture: MTLTexture) throws { 15 | self.device = colorTexture.device 16 | self.size = size 17 | self.colorTexture = colorTexture 18 | self.depthTexture = depthTexture 19 | 20 | let renderPassDescriptor = MTLRenderPassDescriptor() 21 | renderPassDescriptor.colorAttachments[0].texture = colorTexture 22 | renderPassDescriptor.colorAttachments[0].loadAction = .clear 23 | renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1) 24 | renderPassDescriptor.colorAttachments[0].storeAction = .store 25 | renderPassDescriptor.depthAttachment.texture = depthTexture 26 | renderPassDescriptor.depthAttachment.loadAction = .clear 27 | renderPassDescriptor.depthAttachment.clearDepth = 1 28 | renderPassDescriptor.depthAttachment.storeAction = .store // TODO: #33 This is hardcoded. Should usually be .dontCare but we need to read back in some examples. 29 | self.renderPassDescriptor = renderPassDescriptor 30 | 31 | commandQueue = try device._makeCommandQueue() 32 | } 33 | 34 | // TODO: #28 Most of this belongs on a RenderSession type API. We should be able to render multiple times with the same setup. 35 | public init(size: CGSize) throws { 36 | let device = _MTLCreateSystemDefaultDevice() 37 | let colorTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm_srgb, width: Int(size.width), height: Int(size.height), mipmapped: false) 38 | colorTextureDescriptor.usage = [.renderTarget, .shaderRead, .shaderWrite] // TODO: #33 this is all hardcoded :-( 39 | let colorTexture = try device.makeTexture(descriptor: colorTextureDescriptor).orThrow(.textureCreationFailure) 40 | colorTexture.label = "Color Texture" 41 | 42 | let depthTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .depth32Float, width: Int(size.width), height: Int(size.height), mipmapped: false) 43 | depthTextureDescriptor.usage = [.renderTarget, .shaderRead] // TODO: #33 this is all hardcoded :-( 44 | let depthTexture = try device.makeTexture(descriptor: depthTextureDescriptor).orThrow(.textureCreationFailure) 45 | depthTexture.label = "Depth Texture" 46 | 47 | try self.init(size: size, colorTexture: colorTexture, depthTexture: depthTexture) 48 | } 49 | 50 | public struct Rendering { 51 | public var texture: MTLTexture 52 | } 53 | } 54 | 55 | public extension OffscreenRenderer { 56 | @MainActor 57 | func render(_ content: Content) throws -> Rendering where Content: Element { 58 | let device = _MTLCreateSystemDefaultDevice() 59 | let commandQueue = try device._makeCommandQueue() 60 | let content = CommandBufferElement(completion: .commitAndWaitUntilCompleted) { 61 | content 62 | } 63 | .environment(\.device, device) 64 | .environment(\.commandQueue, commandQueue) 65 | .environment(\.renderPassDescriptor, renderPassDescriptor) 66 | .environment(\.drawableSize, size) 67 | let graph = try Graph(content: content) 68 | try graph.processSetup() 69 | try graph.processWorkload() 70 | return .init(texture: colorTexture) 71 | } 72 | } 73 | 74 | public extension OffscreenRenderer.Rendering { 75 | var cgImage: CGImage { 76 | get throws { 77 | try texture.toCGImage() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/ShaderLibrary.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import UltraviolenceSupport 3 | 4 | @dynamicMemberLookup 5 | public struct ShaderLibrary { 6 | var library: MTLLibrary 7 | var namespace: String? 8 | 9 | public init(library: MTLLibrary, namespace: String? = nil) { 10 | self.library = library 11 | self.namespace = namespace 12 | } 13 | 14 | public init(bundle: Bundle, namespace: String? = nil) throws { 15 | let device = _MTLCreateSystemDefaultDevice() 16 | 17 | if let url = bundle.url(forResource: "debug", withExtension: "metallib"), let library = try? device.makeLibrary(URL: url) { 18 | self.library = library 19 | } 20 | else { 21 | if let library = try? device.makeDefaultLibrary(bundle: bundle) { 22 | self.library = library 23 | } 24 | else { 25 | throw UltraviolenceError.resourceCreationFailure("Failed to load default library from bundle.") 26 | } 27 | } 28 | self.namespace = namespace 29 | } 30 | 31 | // TODO: #93 Deprecate this for the type safe equivalent 32 | internal func function(named name: String, type: MTLFunctionType? = nil, constantValues: MTLFunctionConstantValues? = nil) throws -> MTLFunction { 33 | let scopedNamed = namespace.map { "\($0)::\(name)" } ?? name 34 | let constantValues = constantValues ?? MTLFunctionConstantValues() 35 | let function = try library.makeFunction(name: scopedNamed, constantValues: constantValues) 36 | if let type, function.functionType != type { 37 | throw UltraviolenceError.resourceCreationFailure("Function \(scopedNamed) is not of type \(type).") 38 | } 39 | return function 40 | } 41 | 42 | public func function(named name: String, type: T.Type, constantValues: MTLFunctionConstantValues? = nil) throws -> T where T: ShaderProtocol { 43 | let scopedNamed = namespace.map { "\($0)::\(name)" } ?? name 44 | let constantValues = constantValues ?? MTLFunctionConstantValues() 45 | let function = try library.makeFunction(name: scopedNamed, constantValues: constantValues) 46 | switch type { 47 | // TODO: #94 Clean this up. 48 | case is VertexShader.Type: 49 | guard function.functionType == .vertex else { 50 | throw UltraviolenceError.resourceCreationFailure("Function \(scopedNamed) is not a vertex function.") 51 | } 52 | return (VertexShader(function) as? T).orFatalError(.resourceCreationFailure("Failed to create VertexShader.")) 53 | case is FragmentShader.Type: 54 | guard function.functionType == .fragment else { 55 | throw UltraviolenceError.resourceCreationFailure("Function \(scopedNamed) is not a fragment function.") 56 | } 57 | return (FragmentShader(function) as? T).orFatalError(.resourceCreationFailure("Failed to create FragmentShader.")) 58 | case is ComputeKernel.Type: 59 | guard function.functionType == .kernel else { 60 | throw UltraviolenceError.resourceCreationFailure("Function \(scopedNamed) is not a kernel function.") 61 | } 62 | return (ComputeKernel(function) as? T).orFatalError(.resourceCreationFailure("Failed to create ComputeKernel.")) 63 | default: 64 | throw UltraviolenceError.resourceCreationFailure("Unknown shader type \(type).") 65 | } 66 | } 67 | } 68 | 69 | public extension ShaderLibrary { 70 | subscript(dynamicMember name: String) -> ComputeKernel { 71 | get throws { 72 | let function = try function(named: name, type: .kernel) 73 | return ComputeKernel(function) 74 | } 75 | } 76 | 77 | subscript(dynamicMember name: String) -> VertexShader { 78 | get throws { 79 | let function = try function(named: name, type: .vertex) 80 | return VertexShader(function) 81 | } 82 | } 83 | 84 | subscript(dynamicMember name: String) -> FragmentShader { 85 | get throws { 86 | let function = try function(named: name, type: .fragment) 87 | return FragmentShader(function) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Shaders.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import UltraviolenceSupport 3 | 4 | public protocol ShaderProtocol { 5 | static var functionType: MTLFunctionType { get } 6 | var function: MTLFunction { get } 7 | init(_ function: MTLFunction) 8 | } 9 | 10 | public extension ShaderProtocol { 11 | init(source: String, logging: Bool = false) throws { 12 | let device = _MTLCreateSystemDefaultDevice() 13 | let options = MTLCompileOptions() 14 | options.enableLogging = logging 15 | let library = try device.makeLibrary(source: source, options: options) 16 | let function = try library.functionNames.compactMap { library.makeFunction(name: $0) }.first { $0.functionType == Self.functionType }.orThrow(.resourceCreationFailure("Failed to create function")) 17 | self.init(function) 18 | } 19 | 20 | init(library: MTLLibrary? = nil, name: String) throws { 21 | let library = try library ?? _MTLCreateSystemDefaultDevice().makeDefaultLibrary().orThrow(.resourceCreationFailure("Failed to create default library")) 22 | let function = try library.makeFunction(name: name).orThrow(.resourceCreationFailure("Failed to create function")) 23 | if function.functionType != .kernel { 24 | throw UltraviolenceError.resourceCreationFailure("Function type is not kernel") 25 | } 26 | self.init(function) 27 | } 28 | } 29 | 30 | // MARK: - 31 | 32 | public struct ComputeKernel: ShaderProtocol { 33 | public static let functionType: MTLFunctionType = .kernel 34 | public var function: MTLFunction 35 | 36 | public init(_ function: MTLFunction) { 37 | self.function = function 38 | } 39 | } 40 | 41 | // MARK: - 42 | 43 | public struct VertexShader: ShaderProtocol { 44 | public static let functionType: MTLFunctionType = .vertex 45 | public var function: MTLFunction 46 | 47 | public init(_ function: MTLFunction) { 48 | self.function = function 49 | } 50 | } 51 | 52 | public extension VertexShader { 53 | var vertexDescriptor: MTLVertexDescriptor? { 54 | function.vertexDescriptor 55 | } 56 | } 57 | 58 | // MARK: - 59 | 60 | public struct FragmentShader: ShaderProtocol { 61 | public static let functionType: MTLFunctionType = .fragment 62 | public var function: MTLFunction 63 | 64 | public init(_ function: MTLFunction) { 65 | self.function = function 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Support/Box.swift: -------------------------------------------------------------------------------- 1 | /// Contain a value with-in a reference type. 2 | @propertyWrapper 3 | internal final class Box { 4 | internal var wrappedValue: Wrapped 5 | 6 | internal init(_ erappedValue: Wrapped) { 7 | self.wrappedValue = erappedValue 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Support/Logging.swift: -------------------------------------------------------------------------------- 1 | internal import Foundation 2 | internal import os 3 | import UltraviolenceSupport 4 | 5 | internal let logger: Logger? = { 6 | guard ProcessInfo.processInfo.loggingEnabled else { 7 | return nil 8 | } 9 | return .init(subsystem: "io.schwa.ultraviolence", category: "default") 10 | }() 11 | 12 | internal let signposter: OSSignposter? = { 13 | guard ProcessInfo.processInfo.loggingEnabled else { 14 | return nil 15 | } 16 | return .init(subsystem: "io.schwa.ultraviolence", category: OSLog.Category.pointsOfInterest) 17 | }() 18 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Support/Support.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | internal import os 3 | import UltraviolenceSupport 4 | 5 | internal extension Element { 6 | var shortDescription: String { 7 | "\(type(of: self))" 8 | } 9 | } 10 | 11 | public extension UltraviolenceError { 12 | static func missingEnvironment(_ key: PartialKeyPath) -> Self { 13 | missingEnvironment("\(key)") 14 | } 15 | } 16 | 17 | @MainActor 18 | internal extension Node { 19 | var shortDescription: String { 20 | self.element?.shortDescription ?? "" 21 | } 22 | } 23 | 24 | public extension Element { 25 | // TODO: #104 Not keen on this being optional. 26 | // TODO: #105 Need a compute variant of this. 27 | func useResource(_ resource: (any MTLResource)?, usage: MTLResourceUsage, stages: MTLRenderStages) -> some Element { 28 | onWorkloadEnter { environmentValues in 29 | if let resource { 30 | let renderCommandEncoder = environmentValues.renderCommandEncoder.orFatalError() 31 | renderCommandEncoder.useResource(resource, usage: usage, stages: stages) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Support/Visitation.swift: -------------------------------------------------------------------------------- 1 | internal extension Graph { 2 | @MainActor 3 | func visit(enter: (Node) throws -> Void, exit: (Node) throws -> Void) throws { 4 | // swiftlint:disable:next no_empty_block 5 | try visit({ _, _ in }, enter: enter, exit: exit) 6 | } 7 | 8 | @MainActor 9 | func visit(_ visitor: (Int, Node) throws -> Void, enter: (Node) throws -> Void, exit: (Node) throws -> Void) throws { 10 | let saved = Graph.current 11 | Graph.current = self 12 | defer { 13 | Graph.current = saved 14 | } 15 | 16 | try root.rebuildIfNeeded() 17 | 18 | assert(activeNodeStack.isEmpty) 19 | 20 | try root.visit(visitor) { node in 21 | activeNodeStack.append(node) 22 | try enter(node) 23 | } 24 | exit: { node in 25 | try exit(node) 26 | activeNodeStack.removeLast() 27 | } 28 | } 29 | } 30 | 31 | internal extension Node { 32 | func visit(depth: Int = 0, _ visitor: (Int, Node) throws -> Void) rethrows { 33 | // swiftlint:disable:next no_empty_block 34 | try visit(depth: depth, visitor, enter: { _ in }, exit: { _ in }) 35 | } 36 | 37 | func visit(depth: Int = 0, _ visitor: (Int, Node) throws -> Void, enter: (Node) throws -> Void, exit: (Node) throws -> Void) rethrows { 38 | try enter(self) 39 | try visitor(depth, self) 40 | try children.forEach { child in 41 | try child.visit(depth: depth + 1, visitor, enter: enter, exit: exit) 42 | } 43 | try exit(self) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Support/WeakBox.swift: -------------------------------------------------------------------------------- 1 | /// A box for holding a weak reference to an object. 2 | internal final class WeakBox { 3 | internal weak var wrappedValue: Wrapped? 4 | internal init(_ wrapped: Wrapped) { 5 | self.wrappedValue = wrapped 6 | } 7 | } 8 | 9 | internal extension WeakBox { 10 | func callAsFunction() -> Wrapped? { 11 | wrappedValue 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/Support/isEqual.swift: -------------------------------------------------------------------------------- 1 | internal func isEqual(_ lhs: LHS, _ rhs: RHS) -> Bool { 2 | if let lhs = lhs as? RHS, lhs == rhs { 3 | return true 4 | } 5 | return false 6 | } 7 | 8 | internal func isEqual(_ lhs: Any, _ rhs: Any) -> Bool { 9 | guard let lhs = lhs as? any Equatable else { 10 | return false 11 | } 12 | guard let rhs = rhs as? any Equatable else { 13 | return false 14 | } 15 | return isEqual(lhs, rhs) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/UVEnvironmentValues+Implementation.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import QuartzCore 3 | import UltraviolenceSupport 4 | 5 | public extension UVEnvironmentValues { 6 | // TODO: #114 This is messy and needs organisation and possibly deprecation of unused elements. 7 | @UVEntry var device: MTLDevice? 8 | @UVEntry var commandQueue: MTLCommandQueue? 9 | @UVEntry var commandBuffer: MTLCommandBuffer? 10 | @UVEntry var renderCommandEncoder: MTLRenderCommandEncoder? 11 | @UVEntry var renderPassDescriptor: MTLRenderPassDescriptor? 12 | @UVEntry var renderPipelineDescriptor: MTLRenderPipelineDescriptor? 13 | @UVEntry var renderPipelineState: MTLRenderPipelineState? 14 | @UVEntry var vertexDescriptor: MTLVertexDescriptor? 15 | @UVEntry var depthStencilDescriptor: MTLDepthStencilDescriptor? 16 | @UVEntry var depthStencilState: MTLDepthStencilState? 17 | @UVEntry var computeCommandEncoder: MTLComputeCommandEncoder? 18 | @UVEntry var computePipelineState: MTLComputePipelineState? 19 | @UVEntry var reflection: Reflection? 20 | @UVEntry var colorAttachment0: (MTLTexture, Int)? 21 | @UVEntry var depthAttachment: MTLTexture? 22 | @UVEntry var stencilAttachment: MTLTexture? 23 | @UVEntry var currentDrawable: CAMetalDrawable? 24 | @UVEntry var drawableSize: CGSize? 25 | @UVEntry var blitCommandEncoder: MTLBlitCommandEncoder? 26 | @UVEntry var enableMetalLogging: Bool = false 27 | } 28 | 29 | public extension Element { 30 | func colorAttachment0(_ texture: MTLTexture, index: Int) -> some Element { 31 | environment(\.colorAttachment0, (texture, index)) 32 | } 33 | func depthAttachment(_ texture: MTLTexture) -> some Element { 34 | environment(\.depthAttachment, texture) 35 | } 36 | func stencilAttahcment(_ texture: MTLTexture) -> some Element { 37 | environment(\.stencilAttachment, texture) 38 | } 39 | } 40 | 41 | public extension Element { 42 | func vertexDescriptor(_ vertexDescriptor: MTLVertexDescriptor) -> some Element { 43 | environment(\.vertexDescriptor, vertexDescriptor) 44 | } 45 | 46 | func depthStencilDescriptor(_ depthStencilDescriptor: MTLDepthStencilDescriptor) -> some Element { 47 | environment(\.depthStencilDescriptor, depthStencilDescriptor) 48 | } 49 | 50 | func depthCompare(function: MTLCompareFunction, enabled: Bool) -> some Element { 51 | depthStencilDescriptor(.init(depthCompareFunction: function, isDepthWriteEnabled: enabled)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Ultraviolence/WorkloadModifier.swift: -------------------------------------------------------------------------------- 1 | internal struct WorkloadModifier : Element, BodylessElement, BodylessContentElement where Content: Element { 2 | var content: Content 3 | var _workloadEnter: ((UVEnvironmentValues) throws -> Void)? 4 | 5 | init(content: Content, workloadEnter: ((UVEnvironmentValues) throws -> Void)? = nil) { 6 | self.content = content 7 | self._workloadEnter = workloadEnter 8 | } 9 | 10 | func workloadEnter(_ node: Node) throws { 11 | try _workloadEnter?(node.environmentValues) 12 | } 13 | } 14 | 15 | public extension Element { 16 | func onWorkloadEnter(_ action: @escaping (UVEnvironmentValues) throws -> Void) -> some Element { 17 | WorkloadModifier(content: self, workloadEnter: action) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/UltraviolenceMacros/UVEntryMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | 5 | public struct UVEntryMacro: AccessorMacro, PeerMacro { 6 | public static func expansion(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] { 7 | // NOTE: The real macro complains if we use it wrong: "'@Entry' macro can only attach to var declarations inside extensions of EnvironmentValues, Transaction, ContainerValues, or FocusedValues" 8 | guard let binding = declaration.as(VariableDeclSyntax.self)?.bindings.first else { 9 | return [] 10 | } 11 | guard let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { 12 | return [] 13 | } 14 | return [ 15 | """ 16 | get { 17 | self[__Key_\(raw: name).self] 18 | } 19 | """, 20 | """ 21 | set { 22 | self[__Key_\(raw: name).self] = newValue 23 | } 24 | """ 25 | ] 26 | } 27 | 28 | public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] { 29 | guard let binding = declaration.as(VariableDeclSyntax.self)?.bindings.first else { 30 | return [] 31 | } 32 | guard let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { 33 | return [] 34 | } 35 | guard let typeAnnotation = binding.typeAnnotation else { 36 | return [] 37 | } 38 | let type = typeAnnotation.type 39 | let defaultValue = binding.initializer?.value ?? "nil" 40 | return [ 41 | """ 42 | private struct __Key_\(raw: name): UVEnvironmentKey { 43 | typealias Value = \(raw: type) 44 | static var defaultValue: Value { \(raw: defaultValue) } 45 | } 46 | """ 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/UltraviolenceMacros/UltraviolenceMacros.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | public struct UltraviolenceMacros: CompilerPlugin { 6 | public let providingMacros: [Macro.Type] = [ 7 | UVEntryMacro.self 8 | ] 9 | 10 | public init() { 11 | // This line intentionaly left blank. 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/UltraviolenceSupport/BaseSupport.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import MetalKit 3 | 4 | public func isPOD(_ type: T.Type) -> Bool { 5 | _isPOD(type) 6 | } 7 | 8 | public func isPOD(_: T) -> Bool { 9 | _isPOD(T.self) 10 | } 11 | 12 | public func isPODArray(_: [T]) -> Bool { 13 | _isPOD(T.self) 14 | } 15 | 16 | public func unreachable(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) -> Never { 17 | fatalError(message(), file: file, line: line) 18 | } 19 | 20 | public extension NSObject { 21 | func copyWithType(_ type: T.Type) -> T where T: NSObject { 22 | (copy() as? T).orFatalError() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/UltraviolenceSupport/Error.swift: -------------------------------------------------------------------------------- 1 | public indirect enum UltraviolenceError: Error, Equatable { 2 | case undefined 3 | case generic(String) 4 | case missingEnvironment(String) 5 | case missingBinding(String) 6 | case resourceCreationFailure(String) 7 | case deviceCababilityFailure(String) 8 | case textureCreationFailure 9 | // TODO: #92 This should be more "impossible" than "unexpected". 10 | case unexpectedError(Self) 11 | case noCurrentGraph 12 | } 13 | 14 | public extension Optional { 15 | func orThrow(_ error: @autoclosure () -> UltraviolenceError) throws -> Wrapped { 16 | // swiftlint:disable:next self_binding 17 | guard let value = self else { 18 | throw error() 19 | } 20 | return value 21 | } 22 | 23 | func orFatalError(_ message: @autoclosure () -> String = String()) -> Wrapped { 24 | // swiftlint:disable:next self_binding 25 | guard let value = self else { 26 | fatalError(message()) 27 | } 28 | return value 29 | } 30 | 31 | func orFatalError(_ error: @autoclosure () -> UltraviolenceError) -> Wrapped { 32 | // swiftlint:disable:next self_binding 33 | guard let value = self else { 34 | fatalError("\(error())") 35 | } 36 | return value 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/UltraviolenceSupport/Logging.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os 3 | 4 | internal let logger: Logger? = { 5 | guard ProcessInfo.processInfo.loggingEnabled else { 6 | return nil 7 | } 8 | return .init(subsystem: "io.schwa.ultraviolence-support", category: "default") 9 | }() 10 | 11 | internal let signposter: OSSignposter? = { 12 | guard ProcessInfo.processInfo.loggingEnabled else { 13 | return nil 14 | } 15 | return .init(subsystem: "io.schwa.ultraviolence-support", category: OSLog.Category.pointsOfInterest) 16 | }() 17 | 18 | public func withIntervalSignpost(_ signposter: OSSignposter?, name: StaticString, id: OSSignpostID? = nil, around task: () throws -> T) rethrows -> T { 19 | guard let signposter else { 20 | return try task() 21 | } 22 | return try signposter.withIntervalSignpost(name, id: id ?? .exclusive, around: task) 23 | } 24 | 25 | public extension ProcessInfo { 26 | var loggingEnabled: Bool { 27 | return true 28 | // guard let value = environment["LOGGING"]?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() else { 29 | // return false 30 | // } 31 | // return ["yes", "true", "y", "1", "on"].contains(value) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/UltraviolenceSupport/MetalSupport.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable file_length 2 | 3 | import CoreGraphics 4 | import ImageIO 5 | import Metal 6 | import MetalKit 7 | import ModelIO 8 | import simd 9 | import SwiftUI 10 | import UniformTypeIdentifiers 11 | #if canImport(AppKit) 12 | import AppKit 13 | #elseif canImport(UIKit) 14 | import UIKit 15 | #endif 16 | 17 | public extension MTLVertexDescriptor { 18 | convenience init(vertexAttributes: [MTLVertexAttribute]) { 19 | self.init() 20 | var offset: Int = 0 21 | for (index, attribute) in vertexAttributes.enumerated() { 22 | let format = MTLVertexFormat(attribute.attributeType) 23 | attributes[index].format = format 24 | attributes[index].bufferIndex = 0 25 | attributes[index].offset = offset 26 | offset += format.size(packed: true) 27 | } 28 | layouts[0].stride = offset 29 | } 30 | } 31 | 32 | public extension MTLVertexFormat { 33 | init(_ dataType: MTLDataType) { 34 | switch dataType { 35 | case .float3: 36 | self = .float3 37 | 38 | case .float2: 39 | self = .float2 40 | 41 | default: 42 | fatalError("Unimplemented") 43 | } 44 | } 45 | 46 | func size(packed: Bool) -> Int { 47 | switch self { 48 | case .float3: 49 | return packed ? MemoryLayout.stride * 3 : MemoryLayout>.size 50 | 51 | case .float2: 52 | return MemoryLayout>.size 53 | 54 | default: 55 | fatalError("Unimplemented") 56 | } 57 | } 58 | } 59 | 60 | public extension MTLDepthStencilDescriptor { 61 | convenience init(depthCompareFunction: MTLCompareFunction, isDepthWriteEnabled: Bool = true) { 62 | self.init() 63 | self.depthCompareFunction = depthCompareFunction 64 | self.isDepthWriteEnabled = isDepthWriteEnabled 65 | } 66 | 67 | convenience init(isDepthWriteEnabled: Bool = true) { 68 | self.init() 69 | self.isDepthWriteEnabled = isDepthWriteEnabled 70 | } 71 | } 72 | 73 | public extension MTLCaptureManager { 74 | func with(enabled: Bool = true, _ body: () throws -> R) throws -> R { 75 | guard enabled else { 76 | return try body() 77 | } 78 | let device = _MTLCreateSystemDefaultDevice() 79 | let captureScope = makeCaptureScope(device: device) 80 | let captureDescriptor = MTLCaptureDescriptor() 81 | captureDescriptor.captureObject = captureScope 82 | try startCapture(with: captureDescriptor) 83 | captureScope.begin() 84 | defer { 85 | captureScope.end() 86 | } 87 | return try body() 88 | } 89 | } 90 | 91 | public extension MTLCommandBuffer { 92 | func withDebugGroup(enabled: Bool = true, _ label: String, _ body: () throws -> R) rethrows -> R { 93 | guard enabled else { 94 | return try body() 95 | } 96 | pushDebugGroup(label) 97 | defer { 98 | popDebugGroup() 99 | } 100 | return try body() 101 | } 102 | } 103 | 104 | public extension MTLRenderCommandEncoder { 105 | func withDebugGroup(enabled: Bool = true, _ label: String, _ body: () throws -> R) rethrows -> R { 106 | guard enabled else { 107 | return try body() 108 | } 109 | pushDebugGroup(label) 110 | defer { 111 | popDebugGroup() 112 | } 113 | return try body() 114 | } 115 | } 116 | 117 | public extension MTLComputeCommandEncoder { 118 | func withDebugGroup(enabled: Bool = true, _ label: String, _ body: () throws -> R) rethrows -> R { 119 | guard enabled else { 120 | return try body() 121 | } 122 | pushDebugGroup(label) 123 | defer { 124 | popDebugGroup() 125 | } 126 | return try body() 127 | } 128 | } 129 | 130 | public extension MTLBlitCommandEncoder { 131 | func withDebugGroup(enabled: Bool = true, label: String, _ body: () throws -> R) rethrows -> R { 132 | guard enabled else { 133 | return try body() 134 | } 135 | pushDebugGroup(label) 136 | defer { 137 | popDebugGroup() 138 | } 139 | return try body() 140 | } 141 | } 142 | 143 | public enum MTLCommandQueueCompletion { 144 | case none 145 | case commit 146 | case commitAndWaitUntilCompleted 147 | } 148 | 149 | public extension MTLCommandQueue { 150 | func labeled(_ label: String) -> Self { 151 | self.label = label 152 | return self 153 | } 154 | } 155 | 156 | public extension MTLCommandBuffer { 157 | func labeled(_ label: String) -> Self { 158 | self.label = label 159 | return self 160 | } 161 | } 162 | 163 | public extension MTLRenderCommandEncoder { 164 | func labeled(_ label: String) -> Self { 165 | self.label = label 166 | return self 167 | } 168 | } 169 | 170 | public extension MTLTexture { 171 | func labeled(_ label: String) -> Self { 172 | self.label = label 173 | return self 174 | } 175 | } 176 | 177 | public extension MTLBuffer { 178 | func labeled(_ label: String) -> Self { 179 | self.label = label 180 | return self 181 | } 182 | } 183 | 184 | public extension MTLRenderCommandEncoder { 185 | func setVertexBuffers(of mesh: MTKMesh) { 186 | for (index, vertexBuffer) in mesh.vertexBuffers.enumerated() { 187 | setVertexBuffer(vertexBuffer.buffer, offset: vertexBuffer.offset, index: index) 188 | } 189 | } 190 | 191 | func draw(_ mesh: MTKMesh) { 192 | for submesh in mesh.submeshes { 193 | draw(submesh) 194 | } 195 | } 196 | 197 | func draw(_ submesh: MTKSubmesh) { 198 | drawIndexedPrimitives(type: submesh.primitiveType, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: submesh.indexBuffer.offset) 199 | } 200 | 201 | func draw(_ mesh: MTKMesh, instanceCount: Int) { 202 | for submesh in mesh.submeshes { 203 | draw(submesh, instanceCount: instanceCount) 204 | } 205 | } 206 | 207 | func draw(_ submesh: MTKSubmesh, instanceCount: Int) { 208 | drawIndexedPrimitives(type: submesh.primitiveType, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: submesh.indexBuffer.offset, instanceCount: instanceCount) 209 | } 210 | } 211 | 212 | public extension MTLVertexDescriptor { 213 | convenience init(_ vertexDescriptor: MDLVertexDescriptor) { 214 | self.init() 215 | // swiftlint:disable:next force_cast 216 | for (index, attribute) in vertexDescriptor.attributes.map({ $0 as! MDLVertexAttribute }).enumerated() { 217 | self.attributes[index].format = MTLVertexFormat(attribute.format) 218 | self.attributes[index].offset = attribute.offset 219 | self.attributes[index].bufferIndex = attribute.bufferIndex 220 | } 221 | // swiftlint:disable:next force_cast 222 | for (index, layout) in vertexDescriptor.layouts.map({ $0 as! MDLVertexBufferLayout }).enumerated() { 223 | self.layouts[index].stride = layout.stride 224 | } 225 | } 226 | } 227 | 228 | public extension MTLVertexFormat { 229 | // swiftlint:disable:next function_body_length cyclomatic_complexity 230 | init(_ dataType: MDLVertexFormat) { 231 | switch dataType { 232 | case .invalid: 233 | self = .invalid 234 | 235 | case .uChar: 236 | self = .uchar 237 | 238 | case .uChar2: 239 | self = .uchar2 240 | 241 | case .uChar3: 242 | self = .uchar3 243 | 244 | case .uChar4: 245 | self = .uchar4 246 | 247 | case .char: 248 | self = .char 249 | 250 | case .char2: 251 | self = .char2 252 | 253 | case .char3: 254 | self = .char3 255 | 256 | case .char4: 257 | self = .char4 258 | 259 | case .uCharNormalized: 260 | self = .ucharNormalized 261 | 262 | case .uChar2Normalized: 263 | self = .uchar2Normalized 264 | 265 | case .uChar3Normalized: 266 | self = .uchar3Normalized 267 | 268 | case .uChar4Normalized: 269 | self = .uchar4Normalized 270 | 271 | case .charNormalized: 272 | self = .charNormalized 273 | 274 | case .char2Normalized: 275 | self = .char2Normalized 276 | 277 | case .char3Normalized: 278 | self = .char3Normalized 279 | 280 | case .char4Normalized: 281 | self = .char4Normalized 282 | 283 | case .uShort: 284 | self = .ushort 285 | 286 | case .uShort2: 287 | self = .ushort2 288 | 289 | case .uShort3: 290 | self = .ushort3 291 | 292 | case .uShort4: 293 | self = .ushort4 294 | 295 | case .short: 296 | self = .short 297 | 298 | case .short2: 299 | self = .short2 300 | 301 | case .short3: 302 | self = .short3 303 | 304 | case .short4: 305 | self = .short4 306 | 307 | case .uShortNormalized: 308 | self = .ushortNormalized 309 | 310 | case .uShort2Normalized: 311 | self = .ushort2Normalized 312 | 313 | case .uShort3Normalized: 314 | self = .ushort3Normalized 315 | 316 | case .uShort4Normalized: 317 | self = .ushort4Normalized 318 | 319 | case .shortNormalized: 320 | self = .shortNormalized 321 | 322 | case .short2Normalized: 323 | self = .short2Normalized 324 | 325 | case .short3Normalized: 326 | self = .short3Normalized 327 | 328 | case .short4Normalized: 329 | self = .short4Normalized 330 | 331 | case .uInt: 332 | self = .uint 333 | 334 | case .uInt2: 335 | self = .uint2 336 | 337 | case .uInt3: 338 | self = .uint3 339 | 340 | case .uInt4: 341 | self = .uint4 342 | 343 | case .int: 344 | self = .int 345 | 346 | case .int2: 347 | self = .int2 348 | 349 | case .int3: 350 | self = .int3 351 | 352 | case .int4: 353 | self = .int4 354 | 355 | case .half: 356 | self = .half 357 | 358 | case .half2: 359 | self = .half2 360 | 361 | case .half3: 362 | self = .half3 363 | 364 | case .half4: 365 | self = .half4 366 | 367 | case .float: 368 | self = .float 369 | 370 | case .float2: 371 | self = .float2 372 | 373 | case .float3: 374 | self = .float3 375 | 376 | case .float4: 377 | self = .float4 378 | 379 | case .int1010102Normalized: 380 | self = .int1010102Normalized 381 | 382 | case .uInt1010102Normalized: 383 | self = .uint1010102Normalized 384 | 385 | default: 386 | fatalError("Unimplemented MDLVertexFormat(\(dataType.rawValue))") 387 | } 388 | } 389 | } 390 | 391 | public extension MTLFunction { 392 | var vertexDescriptor: MTLVertexDescriptor? { 393 | // TODO: #53 Probably better to remove this at this point. 394 | logger?.warning("Creating a vertex descriptor from a function is not recommended.") 395 | guard let vertexAttributes else { 396 | return nil 397 | } 398 | let vertexDescriptor = MTLVertexDescriptor() 399 | for attribute in vertexAttributes { 400 | switch attribute.attributeType { 401 | case .float: 402 | vertexDescriptor.attributes[attribute.attributeIndex].format = .float 403 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride 404 | case .float2: 405 | vertexDescriptor.attributes[attribute.attributeIndex].format = .float2 406 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 407 | case .float3: 408 | vertexDescriptor.attributes[attribute.attributeIndex].format = .float3 409 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride * 3 // NOTE: We use the packed size here. 410 | case .float4: 411 | vertexDescriptor.attributes[attribute.attributeIndex].format = .float4 412 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 413 | case .half: 414 | vertexDescriptor.attributes[attribute.attributeIndex].format = .half 415 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride 416 | case .half2: 417 | vertexDescriptor.attributes[attribute.attributeIndex].format = .half2 418 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 419 | case .half3: 420 | vertexDescriptor.attributes[attribute.attributeIndex].format = .half3 421 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride * 3 // NOTE: We use the packed size here. 422 | case .half4: 423 | vertexDescriptor.attributes[attribute.attributeIndex].format = .half4 424 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 425 | case .int: 426 | vertexDescriptor.attributes[attribute.attributeIndex].format = .int 427 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride 428 | case .int2: 429 | vertexDescriptor.attributes[attribute.attributeIndex].format = .int2 430 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 431 | case .int3: 432 | vertexDescriptor.attributes[attribute.attributeIndex].format = .int3 433 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride * 3 // NOTE: We use the packed size here. 434 | case .int4: 435 | vertexDescriptor.attributes[attribute.attributeIndex].format = .int4 436 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 437 | case .uint: 438 | vertexDescriptor.attributes[attribute.attributeIndex].format = .uint 439 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride 440 | case .uint2: 441 | vertexDescriptor.attributes[attribute.attributeIndex].format = .uint2 442 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 443 | case .uint3: 444 | vertexDescriptor.attributes[attribute.attributeIndex].format = .uint3 445 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride * 3 // NOTE: We use the packed size here. 446 | case .uint4: 447 | vertexDescriptor.attributes[attribute.attributeIndex].format = .uint4 448 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 449 | case .short: 450 | vertexDescriptor.attributes[attribute.attributeIndex].format = .short 451 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride 452 | case .short2: 453 | vertexDescriptor.attributes[attribute.attributeIndex].format = .short2 454 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 455 | case .short3: 456 | vertexDescriptor.attributes[attribute.attributeIndex].format = .short3 457 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride * 3 // NOTE: We use the packed size here. 458 | case .short4: 459 | vertexDescriptor.attributes[attribute.attributeIndex].format = .short4 460 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 461 | case .ushort: 462 | vertexDescriptor.attributes[attribute.attributeIndex].format = .ushort 463 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride 464 | case .ushort2: 465 | vertexDescriptor.attributes[attribute.attributeIndex].format = .ushort2 466 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 467 | case .ushort3: 468 | vertexDescriptor.attributes[attribute.attributeIndex].format = .ushort3 469 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride * 3 // NOTE: We use the packed size here. 470 | case .ushort4: 471 | vertexDescriptor.attributes[attribute.attributeIndex].format = .ushort4 472 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 473 | case .char: 474 | vertexDescriptor.attributes[attribute.attributeIndex].format = .char 475 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride 476 | case .char2: 477 | vertexDescriptor.attributes[attribute.attributeIndex].format = .char2 478 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 479 | case .char3: 480 | vertexDescriptor.attributes[attribute.attributeIndex].format = .char3 481 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride * 3 // NOTE: We use the packed size here. 482 | case .char4: 483 | vertexDescriptor.attributes[attribute.attributeIndex].format = .char4 484 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 485 | case .uchar: 486 | vertexDescriptor.attributes[attribute.attributeIndex].format = .uchar 487 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride 488 | case .uchar2: 489 | vertexDescriptor.attributes[attribute.attributeIndex].format = .uchar2 490 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 491 | case .uchar3: 492 | vertexDescriptor.attributes[attribute.attributeIndex].format = .uchar3 493 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout.stride * 3 // NOTE: We use the packed size here. 494 | case .uchar4: 495 | vertexDescriptor.attributes[attribute.attributeIndex].format = .uchar4 496 | vertexDescriptor.layouts[attribute.attributeIndex].stride = MemoryLayout>.stride 497 | default: 498 | // TODO: #53 Flesh this out. 499 | fatalError("Unimplemented: \(attribute.attributeType)") 500 | } 501 | vertexDescriptor.attributes[attribute.attributeIndex].bufferIndex = attribute.attributeIndex 502 | } 503 | return vertexDescriptor 504 | } 505 | } 506 | 507 | public extension MTLRenderCommandEncoder { 508 | func setVertexUnsafeBytes(of value: [some Any], index: Int) { 509 | precondition(index >= 0) 510 | value.withUnsafeBytes { buffer in 511 | let baseAddress = buffer.baseAddress.orFatalError(.resourceCreationFailure("No base address.")) 512 | setVertexBytes(baseAddress, length: buffer.count, index: index) 513 | } 514 | } 515 | 516 | func setVertexUnsafeBytes(of value: some Any, index: Int) { 517 | precondition(index >= 0) 518 | assert(isPOD(value)) 519 | withUnsafeBytes(of: value) { buffer in 520 | let baseAddress = buffer.baseAddress.orFatalError(.resourceCreationFailure("No base address.")) 521 | setVertexBytes(baseAddress, length: buffer.count, index: index) 522 | } 523 | } 524 | } 525 | 526 | public extension MTLRenderCommandEncoder { 527 | func setFragmentUnsafeBytes(of value: [some Any], index: Int) { 528 | precondition(index >= 0) 529 | value.withUnsafeBytes { buffer in 530 | let baseAddress = buffer.baseAddress.orFatalError(.resourceCreationFailure("No base address.")) 531 | setFragmentBytes(baseAddress, length: buffer.count, index: index) 532 | } 533 | } 534 | 535 | func setFragmentUnsafeBytes(of value: some Any, index: Int) { 536 | precondition(index >= 0) 537 | assert(isPOD(value)) 538 | withUnsafeBytes(of: value) { buffer in 539 | let baseAddress = buffer.baseAddress.orFatalError(.resourceCreationFailure("No base address.")) 540 | setFragmentBytes(baseAddress, length: buffer.count, index: index) 541 | } 542 | } 543 | } 544 | 545 | public extension MTLRenderCommandEncoder { 546 | func setUnsafeBytes(of value: [some Any], index: Int, functionType: MTLFunctionType) { 547 | precondition(index >= 0) 548 | switch functionType { 549 | case .vertex: 550 | setVertexUnsafeBytes(of: value, index: index) 551 | 552 | case .fragment: 553 | setFragmentUnsafeBytes(of: value, index: index) 554 | 555 | default: 556 | fatalError("Unimplemented") 557 | } 558 | } 559 | 560 | func setUnsafeBytes(of value: some Any, index: Int, functionType: MTLFunctionType) { 561 | precondition(index >= 0) 562 | assert(isPOD(value)) 563 | switch functionType { 564 | case .vertex: 565 | setVertexUnsafeBytes(of: value, index: index) 566 | 567 | case .fragment: 568 | setFragmentUnsafeBytes(of: value, index: index) 569 | 570 | default: 571 | fatalError("Unimplemented") 572 | } 573 | } 574 | 575 | func setBuffer(_ buffer: MTLBuffer?, offset: Int, index: Int, functionType: MTLFunctionType) { 576 | switch functionType { 577 | case .vertex: 578 | setVertexBuffer(buffer, offset: offset, index: index) 579 | 580 | case .fragment: 581 | setFragmentBuffer(buffer, offset: offset, index: index) 582 | 583 | default: 584 | fatalError("Unimplemented") 585 | } 586 | } 587 | 588 | func setTexture(_ texture: MTLTexture?, index: Int, functionType: MTLFunctionType) { 589 | switch functionType { 590 | case .vertex: 591 | setVertexTexture(texture, index: index) 592 | 593 | case .fragment: 594 | setFragmentTexture(texture, index: index) 595 | 596 | default: 597 | fatalError("Unimplemented") 598 | } 599 | } 600 | 601 | func setSamplerState(_ sampler: MTLSamplerState?, index: Int, functionType: MTLFunctionType) { 602 | switch functionType { 603 | case .vertex: 604 | setVertexSamplerState(sampler, index: index) 605 | 606 | case .fragment: 607 | setFragmentSamplerState(sampler, index: index) 608 | 609 | default: 610 | fatalError("Unimplemented") 611 | } 612 | } 613 | } 614 | 615 | public extension MTLComputeCommandEncoder { 616 | func setUnsafeBytes(of value: [some Any], index: Int) { 617 | precondition(index >= 0) 618 | value.withUnsafeBytes { buffer in 619 | let baseAddress = buffer.baseAddress.orFatalError(.resourceCreationFailure("No base address.")) 620 | setBytes(baseAddress, length: buffer.count, index: index) 621 | } 622 | } 623 | 624 | func setUnsafeBytes(of value: some Any, index: Int) { 625 | precondition(index >= 0) 626 | assert(isPOD(value)) 627 | withUnsafeBytes(of: value) { buffer in 628 | let baseAddress = buffer.baseAddress.orFatalError(.resourceCreationFailure("No base address.")) 629 | setBytes(baseAddress, length: buffer.count, index: index) 630 | } 631 | } 632 | } 633 | 634 | // TODO: #118 Sanitize 635 | public extension MTLDevice { 636 | func makeBuffer(unsafeBytesOf value: T, options: MTLResourceOptions = []) throws -> MTLBuffer { 637 | precondition(isPOD(value)) 638 | return try withUnsafeBytes(of: value) { buffer in 639 | let baseAddress = buffer.baseAddress.orFatalError(.resourceCreationFailure("No base address.")) 640 | return try makeBuffer(bytes: baseAddress, length: buffer.count, options: options).orThrow(.resourceCreationFailure("TODO")) 641 | } 642 | } 643 | 644 | func makeBuffer(unsafeBytesOf value: [T], options: MTLResourceOptions = []) throws -> MTLBuffer { 645 | precondition(isPODArray(value)) 646 | return try value.withUnsafeBytes { buffer in 647 | let baseAddress = buffer.baseAddress.orFatalError(.resourceCreationFailure("No base address.")) 648 | return try makeBuffer(bytes: baseAddress, length: buffer.count, options: options).orThrow(.resourceCreationFailure("TODO")) 649 | } 650 | } 651 | 652 | func makeBuffer(collection: C, options: MTLResourceOptions) throws -> MTLBuffer where C: Collection { 653 | assert(isPOD(C.Element.self)) 654 | let buffer = try collection.withContiguousStorageIfAvailable { buffer in 655 | let raw = UnsafeRawBufferPointer(buffer) 656 | let baseAddress = raw.baseAddress.orFatalError(.resourceCreationFailure("No base address.")) 657 | guard let buffer = makeBuffer(bytes: baseAddress, length: raw.count, options: options) else { 658 | throw UltraviolenceError.resourceCreationFailure("MTLDevice.makeBuffer failed.") 659 | } 660 | return buffer 661 | } 662 | guard let buffer else { 663 | fatalError("No contiguous storage available.") 664 | } 665 | return buffer 666 | } 667 | } 668 | 669 | public extension MTLBuffer { 670 | func contentsBuffer() -> UnsafeRawBufferPointer { 671 | UnsafeRawBufferPointer(start: contents(), count: length) 672 | } 673 | 674 | func contents() -> UnsafeBufferPointer { 675 | contentsBuffer().bindMemory(to: T.self) 676 | } 677 | } 678 | 679 | public extension MTLCommandBufferDescriptor { 680 | func addDefaultLogging() throws { 681 | let logStateDescriptor = MTLLogStateDescriptor() 682 | logStateDescriptor.bufferSize = 16 * 1_024 683 | let device = _MTLCreateSystemDefaultDevice() 684 | let logState = try device.makeLogState(descriptor: logStateDescriptor) 685 | logState.addLogHandler { _, _, _, message in 686 | logger?.log("\(message)") 687 | } 688 | self.logState = logState 689 | } 690 | } 691 | 692 | public extension SIMD2 { 693 | init(_ size: CGSize) { 694 | self.init(Float(size.width), Float(size.height)) 695 | } 696 | } 697 | 698 | public func _MTLCreateSystemDefaultDevice() -> MTLDevice { 699 | MTLCreateSystemDefaultDevice().orFatalError(.unexpectedError(.resourceCreationFailure("Could not create system default device."))) 700 | } 701 | 702 | public extension MTLDevice { 703 | func _makeCommandQueue() throws -> MTLCommandQueue { 704 | try makeCommandQueue().orThrow(.resourceCreationFailure("Could not create command queue.")) 705 | } 706 | 707 | func _makeSamplerState(descriptor: MTLSamplerDescriptor) throws -> MTLSamplerState { 708 | try makeSamplerState(descriptor: descriptor).orThrow(.resourceCreationFailure("Could not create sampler state.")) 709 | } 710 | 711 | func _makeTexture(descriptor: MTLTextureDescriptor) throws -> MTLTexture { 712 | try makeTexture(descriptor: descriptor).orThrow(.resourceCreationFailure("Could not create texture.")) 713 | } 714 | } 715 | 716 | public extension MTLCommandQueue { 717 | func _makeCommandBuffer() throws -> MTLCommandBuffer { 718 | try makeCommandBuffer().orThrow(.resourceCreationFailure("Could not create command buffer.")) 719 | } 720 | 721 | func _makeCommandBuffer(descriptor: MTLCommandBufferDescriptor) throws -> MTLCommandBuffer { 722 | try makeCommandBuffer(descriptor: descriptor).orThrow(.resourceCreationFailure("Could not create command buffer.")) 723 | } 724 | } 725 | 726 | public extension MTLCommandBuffer { 727 | func _makeBlitCommandEncoder() throws -> MTLBlitCommandEncoder { 728 | try makeBlitCommandEncoder().orThrow(.resourceCreationFailure("Could not create blit command encoder.")) 729 | } 730 | 731 | func _makeComputeCommandEncoder() throws -> MTLComputeCommandEncoder { 732 | try makeComputeCommandEncoder().orThrow(.resourceCreationFailure("Could not create compute command encoder.")) 733 | } 734 | 735 | func _makeRenderCommandEncoder(descriptor: MTLRenderPassDescriptor) throws -> MTLRenderCommandEncoder { 736 | try makeRenderCommandEncoder(descriptor: descriptor).orThrow(.resourceCreationFailure("Could not create render command encoder.")) 737 | } 738 | } 739 | 740 | public extension MTLDevice { 741 | // Deal with your own endian problems. 742 | // TODO: #119 Rename 743 | func makeTexture(descriptor: MTLTextureDescriptor, value: T) throws -> MTLTexture { 744 | assert(isPOD(value)) 745 | let numPixels = descriptor.width * descriptor.height 746 | let values = [T](repeating: value, count: numPixels) 747 | let texture = try _makeTexture(descriptor: descriptor) 748 | values.withUnsafeBufferPointer { buffer in 749 | let buffer = UnsafeRawBufferPointer(buffer) 750 | let baseAddress = buffer.baseAddress.orFatalError() 751 | texture.replace(region: MTLRegionMake2D(0, 0, descriptor.width, descriptor.height), mipmapLevel: 0, withBytes: baseAddress, bytesPerRow: descriptor.width * MemoryLayout.stride) 752 | } 753 | return texture 754 | } 755 | 756 | func make1PixelTexture(color: SIMD4) throws -> MTLTexture { 757 | let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: 1, height: 1, mipmapped: false) 758 | descriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] // TODO: #120 Too much 759 | descriptor.storageMode = .shared 760 | let value = SIMD4(color * 255.0) 761 | return try makeTexture(descriptor: descriptor, value: value) 762 | } 763 | } 764 | 765 | public extension MTLTexture { 766 | func toCGImage() throws -> CGImage { 767 | // TODO: #121 Hack 768 | assert(self.pixelFormat == .bgra8Unorm || self.pixelFormat == .bgra8Unorm_srgb) 769 | var bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue) 770 | bitmapInfo.insert(.byteOrder32Little) 771 | let context = try CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo.rawValue).orThrow(.resourceCreationFailure("Failed to create context.")) 772 | let data = try context.data.orThrow(.resourceCreationFailure("Failed to get context data.")) 773 | getBytes(data, bytesPerRow: width * 4, from: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0) 774 | return try context.makeImage().orThrow(.resourceCreationFailure("Failed to create image.")) 775 | } 776 | 777 | func toImage() throws -> Image { 778 | #if canImport(AppKit) 779 | let nsImage = NSImage(cgImage: try toCGImage(), size: CGSize(width: width, height: height)) 780 | return Image(nsImage: nsImage) 781 | #elseif canImport(UIKit) 782 | let cgImage = try toCGImage() 783 | let uiImage = UIImage(cgImage: cgImage) 784 | return Image(uiImage: uiImage) 785 | #endif 786 | } 787 | 788 | func write(to url: URL) throws { 789 | let image = try toCGImage() 790 | // TODO: #122 We're ignoring the file extension. 791 | let destination = try CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil).orThrow(.resourceCreationFailure("Failed to create image destination")) 792 | CGImageDestinationAddImage(destination, image, nil) 793 | CGImageDestinationFinalize(destination) 794 | } 795 | } 796 | 797 | public extension MTLStencilDescriptor { 798 | convenience init(compareFunction: MTLCompareFunction = .always, stencilFailureOperation: MTLStencilOperation = .keep, depthFailureOperation: MTLStencilOperation = .keep, stencilPassDepthPassOperation: MTLStencilOperation = .keep, readMask: UInt32 = 0xffffffff, writeMask: UInt32 = 0xffffffff) { 799 | self.init() 800 | self.stencilCompareFunction = compareFunction 801 | self.stencilFailureOperation = stencilFailureOperation 802 | self.depthFailureOperation = depthFailureOperation 803 | self.depthStencilPassOperation = stencilPassDepthPassOperation 804 | self.readMask = readMask 805 | self.writeMask = writeMask 806 | } 807 | } 808 | 809 | public extension MTLDepthStencilDescriptor { 810 | convenience init(depthCompareFunction: MTLCompareFunction = .less, isDepthWriteEnabled: Bool = true, frontFaceStencil: MTLStencilDescriptor? = nil, backFaceStencil: MTLStencilDescriptor? = nil, label: String? = nil) { 811 | self.init() 812 | self.depthCompareFunction = depthCompareFunction 813 | self.isDepthWriteEnabled = isDepthWriteEnabled 814 | if let frontFaceStencil { 815 | self.frontFaceStencil = frontFaceStencil 816 | } 817 | if let backFaceStencil { 818 | self.backFaceStencil = backFaceStencil 819 | } 820 | if let label { 821 | self.label = label 822 | } 823 | } 824 | } 825 | 826 | public extension MTLSamplerDescriptor { 827 | // swiftlint:disable discouraged_optional_boolean 828 | // swiftlint:disable:next cyclomatic_complexity 829 | convenience init(minFilter: MTLSamplerMinMagFilter? = nil, magFilter: MTLSamplerMinMagFilter? = nil, mipFilter: MTLSamplerMipFilter? = nil, maxAnisotropy: Int? = nil, sAddressMode: MTLSamplerAddressMode? = nil, tAddressMode: MTLSamplerAddressMode? = nil, rAddressMode: MTLSamplerAddressMode? = nil, borderColor: MTLSamplerBorderColor? = nil, normalizedCoordinates: Bool? = nil, lodMinClamp: Float? = nil, lodMaxClamp: Float? = nil, lodAverage: Bool? = nil, compareFunction: MTLCompareFunction? = nil, supportArgumentBuffers: Bool? = nil, label: String? = nil) { 830 | // swiftlint:enable discouraged_optional_boolean 831 | self.init() 832 | if let minFilter { 833 | self.minFilter = minFilter 834 | } 835 | if let magFilter { 836 | self.magFilter = magFilter 837 | } 838 | if let mipFilter { 839 | self.mipFilter = mipFilter 840 | } 841 | if let maxAnisotropy { 842 | self.maxAnisotropy = maxAnisotropy 843 | } 844 | if let sAddressMode { 845 | self.sAddressMode = sAddressMode 846 | } 847 | if let tAddressMode { 848 | self.tAddressMode = tAddressMode 849 | } 850 | if let rAddressMode { 851 | self.rAddressMode = rAddressMode 852 | } 853 | if let borderColor { 854 | self.borderColor = borderColor 855 | } 856 | if let normalizedCoordinates { 857 | self.normalizedCoordinates = normalizedCoordinates 858 | } 859 | if let lodMinClamp { 860 | self.lodMinClamp = lodMinClamp 861 | } 862 | if let lodMaxClamp { 863 | self.lodMaxClamp = lodMaxClamp 864 | } 865 | if let lodAverage { 866 | self.lodAverage = lodAverage 867 | } 868 | if let compareFunction { 869 | self.compareFunction = compareFunction 870 | } 871 | if let supportArgumentBuffers { 872 | self.supportArgumentBuffers = supportArgumentBuffers 873 | } 874 | if let label { 875 | self.label = label 876 | } 877 | } 878 | } 879 | 880 | public extension MTKMesh { 881 | func relabeled(_ label: String) -> MTKMesh { 882 | for (index, buffer) in vertexBuffers.enumerated() { 883 | buffer.buffer.label = "\(label)-vertexBuffer-\(index)" 884 | } 885 | for (index, submesh) in submeshes.enumerated() { 886 | submesh.indexBuffer.buffer.label = "\(label)-submesh-indexBuffer-\(index)" 887 | } 888 | return self 889 | } 890 | } 891 | 892 | public extension MTLBuffer { 893 | func gpuAddressAsUnsafeMutablePointer(type: T.Type) -> UnsafeMutablePointer? { 894 | precondition(MemoryLayout.stride == MemoryLayout.stride) 895 | let bits = Int(Int64(bitPattern: gpuAddress)) 896 | return UnsafeMutablePointer(bitPattern: bits) 897 | } 898 | } 899 | -------------------------------------------------------------------------------- /Sources/UltraviolenceSupport/UltraviolenceSupportMacros.swift: -------------------------------------------------------------------------------- 1 | @attached(accessor) 2 | @attached(peer, names: prefixed(__Key_)) 3 | public macro UVEntry() = #externalMacro(module: "UltraviolenceMacros", type: "UVEntryMacro") 4 | -------------------------------------------------------------------------------- /Sources/UltraviolenceUI/Logging.swift: -------------------------------------------------------------------------------- 1 | internal import Foundation 2 | internal import os.log 3 | import UltraviolenceSupport 4 | 5 | internal let logger: Logger? = { 6 | guard ProcessInfo.processInfo.loggingEnabled else { 7 | return nil 8 | } 9 | return .init(subsystem: "io.schwa.ultraviolence-ui", category: "default") 10 | }() 11 | 12 | internal let signposter: OSSignposter? = { 13 | guard ProcessInfo.processInfo.loggingEnabled else { 14 | return nil 15 | } 16 | return .init(subsystem: "io.schwa.ultraviolence-ui", category: OSLog.Category.pointsOfInterest) 17 | }() 18 | -------------------------------------------------------------------------------- /Sources/UltraviolenceUI/MTKView+Environment.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import MetalKit 3 | import SwiftUI 4 | 5 | internal extension EnvironmentValues { 6 | // swiftlint:disable discouraged_optional_boolean 7 | @Entry var metalFramebufferOnly: Bool? 8 | @Entry var metalDepthStencilAttachmentTextureUsage: MTLTextureUsage? 9 | @Entry var metalMultisampleColorAttachmentTextureUsage: MTLTextureUsage? 10 | @Entry var metalPresentsWithTransaction: Bool? 11 | @Entry var metalColorPixelFormat: MTLPixelFormat? 12 | @Entry var metalDepthStencilPixelFormat: MTLPixelFormat? 13 | @Entry var metalDepthStencilStorageMode: MTLStorageMode? 14 | @Entry var metalSampleCount: Int? 15 | @Entry var metalClearColor: MTLClearColor? 16 | @Entry var metalClearDepth: Double? 17 | @Entry var metalClearStencil: UInt32? 18 | @Entry var metalPreferredFramesPerSecond: Int? 19 | @Entry var metalEnableSetNeedsDisplay: Bool? 20 | @Entry var metalAutoResizeDrawable: Bool? 21 | @Entry var metalIsPaused: Bool? 22 | #if os(macOS) 23 | @Entry var metalColorspace: CGColorSpace? 24 | #endif 25 | // swiftlint:enable discouraged_optional_boolean 26 | } 27 | 28 | public extension View { 29 | func metalFramebufferOnly(_ value: Bool) -> some View { 30 | self.environment(\.metalFramebufferOnly, value) 31 | } 32 | func metalDepthStencilAttachmentTextureUsage(_ value: MTLTextureUsage) -> some View { 33 | self.environment(\.metalDepthStencilAttachmentTextureUsage, value) 34 | } 35 | func metalMultisampleColorAttachmentTextureUsage(_ value: MTLTextureUsage) -> some View { 36 | self.environment(\.metalMultisampleColorAttachmentTextureUsage, value) 37 | } 38 | func metalPresentsWithTransaction(_ value: Bool) -> some View { 39 | self.environment(\.metalPresentsWithTransaction, value) 40 | } 41 | func metalColorPixelFormat(_ value: MTLPixelFormat) -> some View { 42 | self.environment(\.metalColorPixelFormat, value) 43 | } 44 | func metalDepthStencilPixelFormat(_ value: MTLPixelFormat) -> some View { 45 | self.environment(\.metalDepthStencilPixelFormat, value) 46 | } 47 | func metalDepthStencilStorageMode(_ value: MTLStorageMode) -> some View { 48 | self.environment(\.metalDepthStencilStorageMode, value) 49 | } 50 | func metalSampleCount(_ value: Int) -> some View { 51 | self.environment(\.metalSampleCount, value) 52 | } 53 | func metalClearColor(_ value: MTLClearColor) -> some View { 54 | self.environment(\.metalClearColor, value) 55 | } 56 | func metalClearDepth(_ value: Double) -> some View { 57 | self.environment(\.metalClearDepth, value) 58 | } 59 | func metalClearStencil(_ value: UInt32) -> some View { 60 | self.environment(\.metalClearStencil, value) 61 | } 62 | func metalPreferredFramesPerSecond(_ value: Int) -> some View { 63 | self.environment(\.metalPreferredFramesPerSecond, value) 64 | } 65 | func metalEnableSetNeedsDisplay(_ value: Bool) -> some View { 66 | self.environment(\.metalEnableSetNeedsDisplay, value) 67 | } 68 | func metalAutoResizeDrawable(_ value: Bool) -> some View { 69 | self.environment(\.metalAutoResizeDrawable, value) 70 | } 71 | func metalIsPaused(_ value: Bool) -> some View { 72 | self.environment(\.metalIsPaused, value) 73 | } 74 | #if os(macOS) 75 | func metalColorspace(_ value: CGColorSpace?) -> some View { 76 | self.environment(\.metalColorspace, value) 77 | } 78 | #endif 79 | } 80 | 81 | extension MTKView { 82 | // swiftlint:disable:next cyclomatic_complexity 83 | func configure(from environment: EnvironmentValues) { 84 | if let value = environment.metalFramebufferOnly { 85 | self.framebufferOnly = value 86 | } 87 | if let value = environment.metalDepthStencilAttachmentTextureUsage { 88 | self.depthStencilAttachmentTextureUsage = value 89 | } 90 | if let value = environment.metalMultisampleColorAttachmentTextureUsage { 91 | self.multisampleColorAttachmentTextureUsage = value 92 | } 93 | if let value = environment.metalPresentsWithTransaction { 94 | self.presentsWithTransaction = value 95 | } 96 | if let value = environment.metalColorPixelFormat { 97 | self.colorPixelFormat = value 98 | } 99 | if let value = environment.metalDepthStencilPixelFormat { 100 | self.depthStencilPixelFormat = value 101 | } 102 | if let value = environment.metalDepthStencilStorageMode { 103 | self.depthStencilStorageMode = value 104 | } 105 | if let value = environment.metalSampleCount { 106 | self.sampleCount = value 107 | } 108 | if let value = environment.metalClearColor { 109 | self.clearColor = value 110 | } 111 | if let value = environment.metalClearDepth { 112 | self.clearDepth = value 113 | } 114 | if let value = environment.metalClearStencil { 115 | self.clearStencil = value 116 | } 117 | if let value = environment.metalPreferredFramesPerSecond { 118 | self.preferredFramesPerSecond = value 119 | } 120 | if let value = environment.metalEnableSetNeedsDisplay { 121 | self.enableSetNeedsDisplay = value 122 | } 123 | if let value = environment.metalAutoResizeDrawable { 124 | self.autoResizeDrawable = value 125 | } 126 | if let value = environment.metalIsPaused { 127 | self.isPaused = value 128 | } 129 | #if os(macOS) 130 | if let value = environment.metalColorspace { 131 | self.colorspace = value 132 | } 133 | #endif 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/UltraviolenceUI/Parameter+SwiftUI.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Ultraviolence 3 | 4 | // TODO: #110 Also it could take a SwiftUI environment(). Also SRGB? 5 | public extension Element { 6 | func parameter(_ name: String, color: Color, functionType: MTLFunctionType? = nil) -> some Element { 7 | let colorspace = CGColorSpaceCreateDeviceRGB() 8 | guard let color = color.resolve(in: .init()).cgColor.converted(to: colorspace, intent: .defaultIntent, options: nil) else { 9 | preconditionFailure("Unimplemented.") 10 | } 11 | guard let components = color.components?.map({ Float($0) }) else { 12 | preconditionFailure("Unimplemented.") 13 | } 14 | let value = SIMD4(components[0], components[1], components[2], components[3]) 15 | return parameter(name, value, functionType: functionType) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/UltraviolenceUI/RenderView.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import MetalKit 3 | import Observation 4 | internal import os 5 | import QuartzCore 6 | import SwiftUI 7 | import Ultraviolence 8 | import UltraviolenceSupport 9 | 10 | public extension EnvironmentValues { 11 | @Entry 12 | var device: MTLDevice? 13 | 14 | @Entry 15 | var commandQueue: MTLCommandQueue? 16 | 17 | @Entry 18 | var drawableSizeChange: ((CGSize) -> Void)? 19 | } 20 | 21 | public extension View { 22 | func onDrawableSizeChange(perform action: @escaping (CGSize) -> Void) -> some View { 23 | environment(\.drawableSizeChange, action) 24 | } 25 | } 26 | 27 | public struct RenderView : View where Content: Element { 28 | var content: () throws -> Content 29 | 30 | @Environment(\.device) 31 | var device 32 | 33 | @Environment(\.commandQueue) 34 | var commandQueue 35 | 36 | public init(@ElementBuilder content: @escaping () throws -> Content) { 37 | self.content = content 38 | } 39 | 40 | public var body: some View { 41 | let device = device ?? _MTLCreateSystemDefaultDevice() 42 | let commandQueue = commandQueue ?? device.makeCommandQueue().orFatalError(.resourceCreationFailure("Failed to create command queue.")) 43 | RenderViewHelper(device: device, commandQueue: commandQueue, content: content) 44 | } 45 | } 46 | 47 | internal struct RenderViewHelper : View where Content: Element { 48 | var device: MTLDevice 49 | var content: () throws -> Content 50 | 51 | @Environment(\.self) 52 | private var environment 53 | 54 | @Environment(\.drawableSizeChange) 55 | private var drawableSizeChange 56 | 57 | @State 58 | private var viewModel: RenderViewViewModel 59 | 60 | init(device: MTLDevice, commandQueue: MTLCommandQueue, @ElementBuilder content: @escaping () throws -> Content) { 61 | do { 62 | self.device = device 63 | self.viewModel = try RenderViewViewModel(device: device, commandQueue: commandQueue, content: content) 64 | self.content = content 65 | } 66 | catch { 67 | preconditionFailure("Failed to create RenderView.ViewModel: \(error)") 68 | } 69 | } 70 | 71 | var body: some View { 72 | ViewAdaptor { 73 | MTKView() 74 | } 75 | update: { view in 76 | view.device = device 77 | view.delegate = viewModel 78 | view.configure(from: environment) 79 | viewModel.content = content 80 | viewModel.drawableSizeChange = drawableSizeChange 81 | } 82 | .modifier(RenderViewDebugViewModifier()) 83 | .environment(viewModel) 84 | } 85 | } 86 | 87 | @Observable 88 | internal class RenderViewViewModel : NSObject, MTKViewDelegate where Content: Element { 89 | @ObservationIgnored 90 | var device: MTLDevice 91 | 92 | @ObservationIgnored 93 | var commandQueue: MTLCommandQueue 94 | 95 | @ObservationIgnored 96 | 97 | var content: () throws -> Content 98 | var lastError: Error? 99 | 100 | @ObservationIgnored 101 | var graph: Graph 102 | 103 | @ObservationIgnored 104 | var needsSetup = true 105 | 106 | @ObservationIgnored 107 | var drawableSizeChange: ((CGSize) -> Void)? 108 | 109 | @ObservationIgnored 110 | var signpostID = signposter?.makeSignpostID() 111 | 112 | @MainActor 113 | init(device: MTLDevice, commandQueue: MTLCommandQueue, content: @escaping () throws -> Content) throws { 114 | self.device = device 115 | self.content = content 116 | self.commandQueue = commandQueue 117 | self.graph = try Graph(content: EmptyElement()) 118 | } 119 | 120 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 121 | // TODO: #45 We may want to actually do graph.processSetup here so that (expensive) setup is not done at render time. But this is made a lot more difficult because we are wrapping the content in CommandBufferElement and a ton of .environment setting. 122 | needsSetup = true 123 | drawableSizeChange?(size) 124 | } 125 | 126 | func draw(in view: MTKView) { 127 | do { 128 | try withIntervalSignpost(signposter, name: "RenderViewViewModel.draw()", id: signpostID) { 129 | let currentDrawable = try view.currentDrawable.orThrow(.generic("No drawable available")) 130 | defer { 131 | currentDrawable.present() 132 | } 133 | let currentRenderPassDescriptor = try view.currentRenderPassDescriptor.orThrow(.generic("No render pass descriptor available")) 134 | let content = try CommandBufferElement(completion: .commit) { 135 | try self.content() 136 | } 137 | .environment(\.device, device) 138 | .environment(\.commandQueue, commandQueue) 139 | .environment(\.renderPassDescriptor, currentRenderPassDescriptor) 140 | .environment(\.renderPipelineDescriptor, MTLRenderPipelineDescriptor()) 141 | .environment(\.currentDrawable, currentDrawable) 142 | .environment(\.drawableSize, view.drawableSize) 143 | 144 | // TODO: #25 Find a way to detect if graph has changed and set needsSetup to true. I am assuming we get a whole new graph every time - can we confirm this is true and too much work is being done? 145 | try graph.update(content: content) 146 | if needsSetup { 147 | try graph.processSetup() 148 | needsSetup = false 149 | } 150 | try graph.processWorkload() 151 | } 152 | } catch { 153 | logger?.error("Error when drawing: \(error)") 154 | lastError = error 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Sources/UltraviolenceUI/RenderViewDebugViewModifier.swift: -------------------------------------------------------------------------------- 1 | import QuartzCore 2 | import SwiftUI 3 | import Ultraviolence 4 | 5 | internal struct RenderViewDebugViewModifier : ViewModifier where Root: Element { 6 | @State 7 | var debugInspectorIsPresented = false 8 | 9 | @Environment(RenderViewViewModel.self) 10 | var viewModel 11 | 12 | @State 13 | var refreshCount = 0 14 | 15 | @State 16 | var selection: NodeListBox? 17 | 18 | func body(content: Content) -> some View { 19 | content 20 | .toolbar { 21 | Toggle("Inspector", systemImage: "ladybug", isOn: $debugInspectorIsPresented) 22 | Button("Refresh", systemImage: "arrow.trianglehead.clockwise") { 23 | refreshCount += 1 24 | } 25 | } 26 | .inspector(isPresented: $debugInspectorIsPresented) { 27 | VStack { 28 | List([NodeListBox(node: viewModel.graph.root)], children: \.children, selection: $selection) { box in 29 | let node = box.node 30 | 31 | Label(node.name, systemImage: "cube").font(.caption2).tag(box) 32 | } 33 | if let node = selection?.node { 34 | ScrollView { 35 | Form { 36 | LabeledContent("ID", value: "\(ObjectIdentifier(node))") 37 | LabeledContent("Name", value: "\(node.debugName)") 38 | LabeledContent("Debug Label", value: "\(node.debugLabel ?? "")") 39 | LabeledContent("Children", value: "\(node.children.count)") 40 | LabeledContent("# State", value: "\(node.stateProperties.count)") 41 | LabeledContent("Element", value: "\(String(describing: node.element))") 42 | // LabeledContent("# Environment", value: "\(node.environmentValues.count)") 43 | } 44 | } 45 | .font(.caption2) 46 | } 47 | } 48 | .id(refreshCount) 49 | .inspectorColumnWidth(min: 200, ideal: 300) 50 | } 51 | .onChange(of: debugInspectorIsPresented) { 52 | refreshCount += 1 53 | } 54 | } 55 | } 56 | 57 | internal struct NodeListBox: Identifiable, Hashable { 58 | var id: ObjectIdentifier { 59 | ObjectIdentifier(node) 60 | } 61 | var node: Node 62 | // swiftlint:disable:next discouraged_optional_collection 63 | var children: [Self]? { 64 | node.children.isEmpty ? nil : node.children.map { Self(node: $0) } 65 | } 66 | 67 | static func == (lhs: Self, rhs: Self) -> Bool { 68 | lhs.id == rhs.id 69 | } 70 | 71 | func hash(into hasher: inout Hasher) { 72 | hasher.combine(id) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/UltraviolenceUI/ViewAdaptor.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | #if os(macOS) 4 | // TODO: #112 Make private 5 | public struct ViewAdaptor: View where ViewType: NSView { 6 | let make: () -> ViewType 7 | let update: (ViewType) -> Void 8 | 9 | public init(make: @escaping () -> ViewType, update: @escaping (ViewType) -> Void) { 10 | self.make = make 11 | self.update = update 12 | } 13 | 14 | public var body: some View { 15 | Representation(make: make, update: update) 16 | } 17 | 18 | struct Representation: NSViewRepresentable { 19 | let make: () -> ViewType 20 | let update: (ViewType) -> Void 21 | 22 | func makeNSView(context: Context) -> ViewType { 23 | make() 24 | } 25 | 26 | func updateNSView(_ nsView: ViewType, context: Context) { 27 | update(nsView) 28 | } 29 | } 30 | } 31 | 32 | #elseif os(iOS) || os(tvOS) 33 | // TODO: #113 Make private 34 | public struct ViewAdaptor: View where ViewType: UIView { 35 | let make: () -> ViewType 36 | let update: (ViewType) -> Void 37 | 38 | public init(make: @escaping () -> ViewType, update: @escaping (ViewType) -> Void) { 39 | self.make = make 40 | self.update = update 41 | } 42 | 43 | public var body: some View { 44 | Representation(make: make, update: update) 45 | } 46 | 47 | struct Representation: UIViewRepresentable { 48 | let make: () -> ViewType 49 | let update: (ViewType) -> Void 50 | 51 | func makeUIView(context: Context) -> ViewType { 52 | make() 53 | } 54 | 55 | func updateUIView(_ uiView: ViewType, context: Context) { 56 | update(uiView) 57 | } 58 | } 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /Tests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - anonymous_argument_in_multiline_closure 3 | - conditional_returns_on_newline 4 | - explicit_top_level_acl 5 | - file_length 6 | - force_cast 7 | - force_try 8 | - force_unwrapping 9 | - function_body_length 10 | - identifier_name 11 | - nesting 12 | - no_empty_block 13 | - todo 14 | - type_body_length 15 | -------------------------------------------------------------------------------- /Tests/UltraviolenceTests/CoreTests.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Testing 3 | @testable import Ultraviolence 4 | import UltraviolenceSupport 5 | 6 | // TODO: #109 Break this up into smaller files. 7 | 8 | struct DemoElement: Element, BodylessElement { 9 | typealias Body = Never 10 | 11 | var title: String 12 | var action: () -> Void 13 | 14 | init(_ title: String, action: @escaping () -> Void = { }) { 15 | self.title = title 16 | self.action = action 17 | } 18 | 19 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 20 | // This line intentionally left blank. 21 | } 22 | } 23 | 24 | extension UVEnvironmentValues { 25 | @UVEntry 26 | var exampleValue: String = "" 27 | } 28 | 29 | final class Model: ObservableObject { 30 | @Published var counter: Int = 0 31 | } 32 | 33 | extension Element { 34 | func debug(_ f: () -> Void) -> some Element { 35 | f() 36 | return self 37 | } 38 | } 39 | 40 | struct ContentRenderPass: Element { 41 | @UVObservedObject var model = Model() 42 | var body: some Element { 43 | DemoElement("\(model.counter)") { 44 | model.counter += 1 45 | } 46 | } 47 | } 48 | 49 | @MainActor 50 | var nestedModel = Model() 51 | 52 | @MainActor 53 | var nestedBodyCount = 0 54 | 55 | @MainActor 56 | var contentRenderPassBodyCount = 0 57 | 58 | @MainActor 59 | var sampleBodyCount = 0 60 | 61 | @Suite(.serialized) 62 | @MainActor 63 | struct UltraviolenceStateTests { 64 | init() { 65 | nestedBodyCount = 0 66 | contentRenderPassBodyCount = 0 67 | nestedModel.counter = 0 68 | sampleBodyCount = 0 69 | } 70 | 71 | @Test 72 | func testUpdate() throws { 73 | let v = ContentRenderPass() 74 | 75 | let graph = try Graph(content: v) 76 | try graph.rebuildIfNeeded() 77 | var demoElement: DemoElement { 78 | graph.element(at: [0], type: DemoElement.self) 79 | } 80 | #expect(demoElement.title == "0") 81 | demoElement.action() 82 | try graph.rebuildIfNeeded() 83 | #expect(demoElement.title == "1") 84 | } 85 | 86 | // MARK: ObservedObject tests 87 | 88 | @Test 89 | func testConstantNested() throws { 90 | @MainActor struct Nested: Element { 91 | var body: some Element { 92 | nestedBodyCount += 1 93 | return DemoElement("Nested DemoElement") 94 | } 95 | } 96 | 97 | struct ContentRenderPass: Element { 98 | @UVObservedObject var model = Model() 99 | var body: some Element { 100 | DemoElement("\(model.counter)") { 101 | model.counter += 1 102 | } 103 | Nested() 104 | .debug { 105 | contentRenderPassBodyCount += 1 106 | } 107 | } 108 | } 109 | 110 | let v = ContentRenderPass() 111 | let graph = try Graph(content: v) 112 | try graph.rebuildIfNeeded() 113 | #expect(contentRenderPassBodyCount == 1) 114 | #expect(nestedBodyCount == 1) 115 | var demoElement: DemoElement { 116 | graph.element(at: [0, 0], type: DemoElement.self) 117 | } 118 | demoElement.action() 119 | try graph.rebuildIfNeeded() 120 | #expect(contentRenderPassBodyCount == 2) 121 | #expect(nestedBodyCount == 1) 122 | } 123 | 124 | @Test 125 | func testChangedNested() throws { 126 | struct Nested: Element { 127 | var counter: Int 128 | var body: some Element { 129 | nestedBodyCount += 1 130 | return DemoElement("Nested DemoElement") 131 | } 132 | } 133 | 134 | struct ContentRenderPass: Element { 135 | @UVObservedObject var model = Model() 136 | var body: some Element { 137 | DemoElement("\(model.counter)") { 138 | model.counter += 1 139 | } 140 | Nested(counter: model.counter) 141 | .debug { 142 | contentRenderPassBodyCount += 1 143 | } 144 | } 145 | } 146 | 147 | let v = ContentRenderPass() 148 | let graph = try Graph(content: v) 149 | try graph.rebuildIfNeeded() 150 | #expect(contentRenderPassBodyCount == 1) 151 | #expect(nestedBodyCount == 1) 152 | var demoElement: DemoElement { 153 | graph.element(at: [0, 0], type: DemoElement.self) 154 | } 155 | demoElement.action() 156 | try graph.rebuildIfNeeded() 157 | #expect(contentRenderPassBodyCount == 2) 158 | #expect(nestedBodyCount == 2) 159 | } 160 | 161 | @Test 162 | func testUnchangedNested() throws { 163 | struct Nested: Element { 164 | var isLarge: Bool = false 165 | var body: some Element { 166 | nestedBodyCount += 1 167 | return DemoElement("Nested DemoElement") 168 | } 169 | } 170 | 171 | struct ContentRenderPass: Element { 172 | @UVObservedObject var model = Model() 173 | var body: some Element { 174 | DemoElement("\(model.counter)") { 175 | model.counter += 1 176 | } 177 | Nested(isLarge: model.counter > 10) 178 | .debug { 179 | contentRenderPassBodyCount += 1 180 | } 181 | } 182 | } 183 | 184 | let v = ContentRenderPass() 185 | let graph = try Graph(content: v) 186 | try graph.rebuildIfNeeded() 187 | #expect(contentRenderPassBodyCount == 1) 188 | #expect(nestedBodyCount == 1) 189 | var demoElement: DemoElement { 190 | graph.element(at: [0, 0], type: DemoElement.self) 191 | } 192 | demoElement.action() 193 | try graph.rebuildIfNeeded() 194 | #expect(contentRenderPassBodyCount == 2) 195 | #expect(nestedBodyCount == 1) 196 | } 197 | 198 | @Test 199 | func testUnchangedNestedWithObservedObject() throws { 200 | struct Nested: Element { 201 | @UVObservedObject var model = nestedModel 202 | var body: some Element { 203 | nestedBodyCount += 1 204 | return DemoElement("Nested DemoElement") 205 | } 206 | } 207 | 208 | struct ContentRenderPass: Element { 209 | @UVObservedObject var model = Model() 210 | var body: some Element { 211 | DemoElement("\(model.counter)") { 212 | model.counter += 1 213 | } 214 | Nested() 215 | .debug { 216 | contentRenderPassBodyCount += 1 217 | } 218 | } 219 | } 220 | 221 | let v = ContentRenderPass() 222 | let graph = try Graph(content: v) 223 | try graph.rebuildIfNeeded() 224 | #expect(contentRenderPassBodyCount == 1) 225 | #expect(nestedBodyCount == 1) 226 | var demoElement: DemoElement { 227 | graph.element(at: [0, 0], type: DemoElement.self) 228 | } 229 | demoElement.action() 230 | try graph.rebuildIfNeeded() 231 | #expect(contentRenderPassBodyCount == 2) 232 | #expect(nestedBodyCount == 1) 233 | } 234 | 235 | @Test 236 | func testBinding1() throws { 237 | struct Nested: Element { 238 | @UVBinding var counter: Int 239 | var body: some Element { 240 | nestedBodyCount += 1 241 | return DemoElement("Nested DemoElement") 242 | } 243 | } 244 | 245 | struct ContentRenderPass: Element { 246 | @UVObservedObject var model = Model() 247 | var body: some Element { 248 | DemoElement("\(model.counter)") { 249 | model.counter += 1 250 | } 251 | Nested(counter: $model.counter) 252 | .debug { 253 | contentRenderPassBodyCount += 1 254 | } 255 | } 256 | } 257 | 258 | let v = ContentRenderPass() 259 | let graph = try Graph(content: v) 260 | try graph.rebuildIfNeeded() 261 | #expect(contentRenderPassBodyCount == 1) 262 | #expect(nestedBodyCount == 1) 263 | var demoElement: DemoElement { 264 | graph.element(at: [0, 0], type: DemoElement.self) 265 | } 266 | demoElement.action() 267 | try graph.rebuildIfNeeded() 268 | #expect(contentRenderPassBodyCount == 2) 269 | #expect(nestedBodyCount == 2) 270 | } 271 | 272 | @Test 273 | func testBinding2() throws { 274 | struct Nested: Element { 275 | @UVBinding var counter: Int 276 | var body: some Element { 277 | nestedBodyCount += 1 278 | return DemoElement("\(counter)") { counter += 1 } 279 | } 280 | } 281 | 282 | struct ContentRenderPass: Element { 283 | @UVObservedObject var model = Model() 284 | var body: some Element { 285 | Nested(counter: $model.counter) 286 | .debug { 287 | contentRenderPassBodyCount += 1 288 | } 289 | } 290 | } 291 | 292 | let v = ContentRenderPass() 293 | let graph = try Graph(content: v) 294 | try graph.rebuildIfNeeded() 295 | var demoElement: DemoElement { 296 | graph.element(at: [0, 0], type: DemoElement.self) 297 | } 298 | #expect(contentRenderPassBodyCount == 1) 299 | #expect(nestedBodyCount == 1) 300 | #expect(demoElement.title == "0") 301 | demoElement.action() 302 | try graph.rebuildIfNeeded() 303 | #expect(contentRenderPassBodyCount == 2) 304 | #expect(nestedBodyCount == 2) 305 | #expect(demoElement.title == "1") 306 | } 307 | 308 | // MARK: State tests 309 | 310 | @Test 311 | func testSimple() throws { 312 | struct Nested: Element { 313 | @UVState private var counter = 0 314 | var body: some Element { 315 | DemoElement("\(counter)") { 316 | counter += 1 317 | } 318 | } 319 | } 320 | 321 | struct Sample: Element { 322 | @UVState private var counter = 0 323 | var body: some Element { 324 | DemoElement("\(counter)") { 325 | counter += 1 326 | } 327 | Nested() 328 | } 329 | } 330 | 331 | let s = Sample() 332 | let graph = try Graph(content: s) 333 | try graph.rebuildIfNeeded() 334 | var demoElement: DemoElement { 335 | graph.element(at: [0, 0], type: DemoElement.self) 336 | } 337 | var nestedDemoElement: DemoElement { 338 | graph.element(at: [0, 1, 0], type: DemoElement.self) 339 | } 340 | #expect(demoElement.title == "0") 341 | #expect(nestedDemoElement.title == "0") 342 | 343 | nestedDemoElement.action() 344 | try graph.rebuildIfNeeded() 345 | 346 | #expect(demoElement.title == "0") 347 | #expect(nestedDemoElement.title == "1") 348 | 349 | demoElement.action() 350 | try graph.rebuildIfNeeded() 351 | 352 | #expect(demoElement.title == "1") 353 | #expect(nestedDemoElement.title == "1") 354 | } 355 | 356 | @Test 357 | func testBindings() throws { 358 | struct Nested: Element { 359 | @UVBinding var counter: Int 360 | var body: some Element { 361 | DemoElement("\(counter)") { 362 | counter += 1 363 | } 364 | } 365 | } 366 | 367 | struct Sample: Element { 368 | @UVState private var counter = 0 369 | var body: some Element { 370 | Nested(counter: $counter) 371 | } 372 | } 373 | 374 | let s = Sample() 375 | let graph = try Graph(content: s) 376 | try graph.rebuildIfNeeded() 377 | var nestedDemoElement: DemoElement { 378 | graph.element(at: [0, 0], type: DemoElement.self) 379 | } 380 | #expect(nestedDemoElement.title == "0") 381 | 382 | nestedDemoElement.action() 383 | try graph.rebuildIfNeeded() 384 | #expect(nestedDemoElement.title == "1") 385 | } 386 | 387 | @Test 388 | func testUnusedBinding() throws { 389 | struct Nested: Element { 390 | @UVBinding var counter: Int 391 | var body: some Element { 392 | DemoElement("") { 393 | counter += 1 394 | } 395 | .debug { nestedBodyCount += 1 } 396 | } 397 | } 398 | 399 | struct Sample: Element { 400 | @UVState private var counter = 0 401 | var body: some Element { 402 | DemoElement("\(counter)") 403 | Nested(counter: $counter) 404 | .debug { sampleBodyCount += 1 } 405 | } 406 | } 407 | 408 | let s = Sample() 409 | let graph = try Graph(content: s) 410 | try graph.rebuildIfNeeded() 411 | var nestedDemoElement: DemoElement { 412 | graph.element(at: [0, 1, 0], type: DemoElement.self) 413 | } 414 | #expect(sampleBodyCount == 1) 415 | #expect(nestedBodyCount == 1) 416 | 417 | nestedDemoElement.action() 418 | try graph.rebuildIfNeeded() 419 | 420 | #expect(sampleBodyCount == 2) 421 | #expect(nestedBodyCount == 1) 422 | } 423 | 424 | // Environment Tests 425 | 426 | @Test 427 | func testEnvironment1() throws { 428 | struct Example1: Element { 429 | var body: some Element { 430 | EnvironmentReader(keyPath: \.exampleValue) { Example2(value: $0) } 431 | .environment(\.exampleValue, "Hello world") 432 | } 433 | } 434 | 435 | struct Example2: Element, BodylessElement { 436 | typealias Body = Never 437 | var value: String 438 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 439 | } 440 | } 441 | 442 | let s = Example1() 443 | let graph = try Graph(content: s) 444 | try graph.rebuildIfNeeded() 445 | #expect(graph.element(at: [0], type: Example2.self).value == "Hello world") 446 | } 447 | 448 | @Test 449 | func testEnvironment2() throws { 450 | struct Example1: Element { 451 | var body: some Element { 452 | Example2() 453 | .environment(\.exampleValue, "Hello world") 454 | } 455 | } 456 | 457 | struct Example2: Element { 458 | @UVEnvironment(\.exampleValue) 459 | var value 460 | var body: some Element { 461 | Example3(value: value) 462 | } 463 | } 464 | 465 | struct Example3: Element, BodylessElement { 466 | typealias Body = Never 467 | var value: String 468 | func _expandNode(_ node: Node, context: ExpansionContext) throws { 469 | } 470 | } 471 | 472 | let g1 = try Graph(content: Example1()) 473 | try g1.rebuildIfNeeded() 474 | #expect(g1.element(at: [0, 0], type: Example3.self).value == "Hello world") 475 | } 476 | 477 | @Test 478 | func testAnyElement() throws { 479 | let e = DemoElement("Hello world").eraseToAnyElement() 480 | let graph = try Graph(content: e) 481 | try graph.rebuildIfNeeded() 482 | #expect(graph.element(at: [0], type: DemoElement.self).title == "Hello world") 483 | } 484 | 485 | @Test 486 | func testModifier() throws { 487 | let root = DemoElement("Hello world").modifier(PassthroughModifier()) 488 | let graph = try Graph(content: root) 489 | try graph.rebuildIfNeeded() 490 | #expect(graph.element(at: [0, 0], type: DemoElement.self).title == "Hello world") 491 | } 492 | } 493 | 494 | @Test 495 | func isEqualTests() throws { 496 | struct NotEquatable { 497 | } 498 | #expect(isEqual(1, 1) == true) 499 | #expect(isEqual(0, 1) == false) 500 | #expect(isEqual(NotEquatable(), 1) == false) 501 | #expect(isEqual(1, NotEquatable()) == false) 502 | #expect(isEqual(NotEquatable(), NotEquatable()) == false) 503 | } 504 | 505 | @Test 506 | @MainActor 507 | func weirdTests() throws { 508 | let d = DemoElement("Nope") 509 | let any = AnyElement(d) 510 | let node = Node() 511 | #expect(throws: UltraviolenceError.noCurrentGraph) { 512 | try any._expandNode(node, context: .init()) 513 | } 514 | } 515 | 516 | @Test 517 | @MainActor 518 | func testOptionalElement() throws { 519 | let element = DemoElement("Hello world") 520 | let optionalElement = DemoElement?(element) 521 | let graph = try Graph(content: optionalElement) 522 | try graph.rebuildIfNeeded() 523 | try graph.dump() 524 | #expect(graph.element(at: [], type: DemoElement.self).title == "Hello world") 525 | } 526 | 527 | @Test 528 | @MainActor 529 | func testElementDump() throws { 530 | let element = DemoElement("Hello world") 531 | var s = "" 532 | try element.dump(to: &s) 533 | s = s.trimmingCharacters(in: .whitespacesAndNewlines) 534 | #expect(s == "DemoElement") 535 | } 536 | 537 | @Test 538 | @MainActor 539 | func testGraphDump() throws { 540 | let root = DemoElement("Hello world") 541 | let graph = try Graph(content: root) 542 | var s = "" 543 | try graph.dump(to: &s) 544 | s = s.trimmingCharacters(in: .whitespacesAndNewlines) 545 | #expect(s == "DemoElement") 546 | } 547 | 548 | @Test 549 | @MainActor 550 | func testComplexGraphDump() throws { 551 | let root = try Group { 552 | DemoElement("1") 553 | DemoElement("2") 554 | DemoElement("3") 555 | try Group { 556 | DemoElement("4") 557 | try Group { 558 | DemoElement("5") 559 | } 560 | } 561 | } 562 | let graph = try Graph(content: root) 563 | var s = "" 564 | try graph.dump(options: [.dumpElement, .dumpNode], to: &s) 565 | s = s.trimmingCharacters(in: .whitespacesAndNewlines) 566 | print(s) 567 | } 568 | -------------------------------------------------------------------------------- /Tests/UltraviolenceTests/Golden Images/RedTriangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schwa/Ultraviolence/f8a719f81e272f096b99e551467a5ae30e865777/Tests/UltraviolenceTests/Golden Images/RedTriangle.png -------------------------------------------------------------------------------- /Tests/UltraviolenceTests/MacroTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | import SwiftSyntaxMacrosTestSupport 5 | import XCTest 6 | 7 | // Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests. 8 | #if canImport(UltraviolenceMacros) 9 | import UltraviolenceMacros 10 | 11 | let testMacros: [String: Macro.Type] = [ 12 | "Entry": UVEntryMacro.self 13 | ] 14 | #endif 15 | 16 | final class EntryMacroTests: XCTestCase { 17 | func testBasicType() throws { 18 | #if canImport(UltraviolenceMacros) 19 | assertMacroExpansion( 20 | """ 21 | extension EnvironmentValues { 22 | @Entry 23 | var name: Int = 42 24 | } 25 | """, 26 | expandedSource: 27 | """ 28 | extension EnvironmentValues { 29 | var name: Int { 30 | get { 31 | self[__Key_name.self] 32 | } 33 | set { 34 | self[__Key_name.self] = newValue 35 | } 36 | } 37 | 38 | private struct __Key_name: UVEnvironmentKey { 39 | typealias Value = Int 40 | static var defaultValue: Value { 41 | 42 42 | } 43 | } 44 | } 45 | """, 46 | macros: testMacros 47 | ) 48 | #else 49 | throw XCTSkip("macros are only supported when running tests for the host platform") 50 | #endif 51 | } 52 | 53 | func testOptionalType() throws { 54 | #if canImport(NotSwiftUIMacros) 55 | assertMacroExpansion( 56 | """ 57 | extension EnvironmentValues { 58 | @Entry 59 | var name: String? 60 | } 61 | """, 62 | expandedSource: 63 | """ 64 | extension EnvironmentValues { 65 | var name: String? { 66 | get { 67 | self[__Key_name.self] 68 | } 69 | set { 70 | self[__Key_name.self] = newValue 71 | } 72 | } 73 | 74 | private struct __Key_name: UVEnvironmentKey { 75 | typealias Value = String? 76 | static var defaultValue: Value { 77 | nil 78 | } 79 | } 80 | } 81 | """, 82 | macros: testMacros 83 | ) 84 | #else 85 | throw XCTSkip("macros are only supported when running tests for the host platform") 86 | #endif 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/UltraviolenceTests/RenderTests.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import MetalKit 3 | import simd 4 | import SwiftUI 5 | import Testing 6 | @testable import Ultraviolence 7 | import UltraviolenceSupport 8 | import UltraviolenceUI 9 | 10 | @Test(.disabled()) 11 | @MainActor 12 | func testRendering() throws { 13 | let source = """ 14 | #include 15 | using namespace metal; 16 | 17 | struct VertexIn { 18 | float2 position [[attribute(0)]]; 19 | }; 20 | 21 | struct VertexOut { 22 | float4 position [[position]]; 23 | }; 24 | 25 | [[vertex]] VertexOut vertex_main( 26 | const VertexIn in [[stage_in]] 27 | ) { 28 | VertexOut out; 29 | out.position = float4(in.position, 0.0, 1.0); 30 | return out; 31 | } 32 | 33 | [[fragment]] float4 fragment_main( 34 | VertexOut in [[stage_in]], 35 | constant float4 &color [[buffer(0)]] 36 | ) { 37 | return color; 38 | } 39 | """ 40 | 41 | let color: SIMD4 = [1, 0, 0, 1] 42 | var gpuTime: Double = -.greatestFiniteMagnitude 43 | var kernelTime: Double = -.greatestFiniteMagnitude 44 | var gotScheduled = false 45 | var gotCompleted = false 46 | 47 | let renderPass = try RenderPass { 48 | let vertexShader = try VertexShader(source: source) 49 | let fragmentShader = try FragmentShader(source: source) 50 | try RenderPipeline(vertexShader: vertexShader, fragmentShader: fragmentShader) { 51 | Draw { encoder in 52 | let vertices: [SIMD2] = [[0, 0.75], [-0.75, -0.75], [0.75, -0.75]] 53 | encoder.setVertexBytes(vertices, length: MemoryLayout>.stride * 3, index: 0) 54 | encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) 55 | } 56 | .parameter("color", color) 57 | } 58 | } 59 | 60 | // TODO: #150 OffscreenRenderer creates own command buffer without giving us a chance to intercept 61 | .onCommandBufferScheduled { _ in 62 | print("**** onCommandBufferScheduled") 63 | gotScheduled = true 64 | } 65 | .onCommandBufferCompleted { commandBuffer in 66 | gpuTime = commandBuffer.gpuEndTime - commandBuffer.gpuStartTime 67 | kernelTime = commandBuffer.kernelEndTime - commandBuffer.kernelStartTime 68 | gotCompleted = true 69 | } 70 | 71 | let offscreenRenderer = try OffscreenRenderer(size: CGSize(width: 1_600, height: 1_200)) 72 | let image = try offscreenRenderer.render(renderPass).cgImage 73 | #expect(try image.isEqualToGoldenImage(named: "RedTriangle")) 74 | 75 | // See above TODO. 76 | // #expect(gotScheduled == true) 77 | // #expect(gotCompleted == true) 78 | // #expect(gpuTime >= 0) 79 | // #expect(kernelTime >= 0) 80 | } 81 | -------------------------------------------------------------------------------- /Tests/UltraviolenceTests/Support.swift: -------------------------------------------------------------------------------- 1 | import Accelerate 2 | import CoreGraphics 3 | import CoreImage 4 | // swiftlint:disable:next duplicate_imports 5 | import CoreImage.CIFilterBuiltins 6 | import Testing 7 | @testable import Ultraviolence 8 | import UltraviolenceSupport 9 | import UniformTypeIdentifiers 10 | #if canImport(AppKit) 11 | import AppKit 12 | #endif 13 | 14 | // swiftlint:disable force_unwrapping 15 | 16 | extension CGImage { 17 | func isEqualToGoldenImage(named name: String) throws -> Bool { 18 | do { 19 | let goldenImage = try goldenImage(named: name) 20 | guard try imageCompare(self, goldenImage) else { 21 | let url = URL(fileURLWithPath: "/tmp/\(name).png") 22 | try self.write(to: url) 23 | url.revealInFinder() 24 | 25 | throw UltraviolenceError.generic("Images are not equal") 26 | } 27 | return true 28 | } 29 | catch { 30 | let url = URL(fileURLWithPath: "/tmp/\(name).png") 31 | try self.write(to: url) 32 | print("Wrote image to \(url)") 33 | return false 34 | } 35 | } 36 | } 37 | 38 | func goldenImage(named name: String) throws -> CGImage { 39 | let url = Bundle.module.resourceURL!.appendingPathComponent("Golden Images").appendingPathComponent(name).appendingPathExtension("png") 40 | let data = try Data(contentsOf: url) 41 | let imageSource = try CGImageSourceCreateWithData(data as CFData, nil).orThrow(.generic("TODO")) 42 | return try CGImageSourceCreateImageAtIndex(imageSource, 0, nil).orThrow(.generic("TODO")) 43 | } 44 | 45 | func imageCompare(_ image1: CGImage, _ image2: CGImage) throws -> Bool { 46 | let ciContext = CIContext() 47 | 48 | let ciImage1 = CIImage(cgImage: image1) 49 | let ciImage2 = CIImage(cgImage: image2) 50 | 51 | let difference = CIFilter.differenceBlendMode() 52 | difference.setValue(ciImage1, forKey: kCIInputImageKey) 53 | difference.setValue(ciImage2, forKey: kCIInputBackgroundImageKey) 54 | 55 | let differenceImage = difference.outputImage! 56 | let differenceCGImage = ciContext.createCGImage(differenceImage, from: differenceImage.extent)! 57 | 58 | let histogram = try Histogram(image: differenceCGImage) 59 | assert(histogram.relativeAlpha[255] == 1.0) 60 | return histogram.relativeRed[0] == 1.0 && histogram.relativeGreen[0] == 1.0 && histogram.relativeBlue[0] == 1.0 61 | } 62 | 63 | extension Graph { 64 | func element(at path: [Int], type: V.Type) -> V { 65 | var node: Node = root 66 | for index in path { 67 | node = node.children[index] 68 | } 69 | return node.element as! V 70 | } 71 | } 72 | 73 | extension CGImage { 74 | func write(to url: URL) throws { 75 | let destination = CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil)! 76 | CGImageDestinationAddImage(destination, self, nil) 77 | CGImageDestinationFinalize(destination) 78 | } 79 | 80 | func toVimage() throws -> vImage.PixelBuffer { 81 | let colorSpace = CGColorSpace(name: CGColorSpace.displayP3)! 82 | var format = vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 8 * 4, colorSpace: colorSpace, bitmapInfo: .init(rawValue: CGImageAlphaInfo.noneSkipFirst.rawValue))! 83 | return try vImage.PixelBuffer(cgImage: self, cgImageFormat: &format, pixelFormat: vImage.Interleaved8x4.self) 84 | } 85 | 86 | static func withColor(colorSpace: CGColorSpace? = nil, red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1.0) -> CGImage { 87 | let colorSpace = colorSpace ?? CGColorSpaceCreateDeviceRGB() 88 | // TODO: #116 These parameters may not be compatible with the passed in color space. 89 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipFirst.rawValue) 90 | let context = CGContext(data: nil, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)! 91 | context.setFillColor(red: red, green: green, blue: blue, alpha: alpha) 92 | context.fill(CGRect(x: 0, y: 0, width: 1, height: 1)) 93 | return context.makeImage()! 94 | } 95 | } 96 | 97 | struct Histogram { 98 | var pixelCount: Int 99 | var red: [Int] 100 | var green: [Int] 101 | var blue: [Int] 102 | var alpha: [Int] 103 | 104 | init(image: CGImage) throws { 105 | let pixelBuffer = try image.toVimage() 106 | pixelCount = image.width * image.height 107 | let histogram = pixelBuffer.histogram() 108 | alpha = histogram.0.map { Int($0) } 109 | red = histogram.1.map { Int($0) } 110 | green = histogram.2.map { Int($0) } 111 | blue = histogram.3.map { Int($0) } 112 | } 113 | 114 | var peaks: (red: Double, green: Double, blue: Double, alpha: Double) { 115 | func peak(_ channel: [Int]) -> Double { 116 | let max = channel.max()! 117 | let index = channel.firstIndex(of: max)! 118 | return Double(index) / Double(channel.count - 1) 119 | } 120 | return (peak(red), peak(green), peak(blue), peak(alpha)) 121 | } 122 | 123 | var relativeRed: [Double] { 124 | red.map { Double($0) / Double(pixelCount) } 125 | } 126 | 127 | var relativeGreen: [Double] { 128 | green.map { Double($0) / Double(pixelCount) } 129 | } 130 | 131 | var relativeBlue: [Double] { 132 | blue.map { Double($0) / Double(pixelCount) } 133 | } 134 | 135 | var relativeAlpha: [Double] { 136 | alpha.map { Double($0) / Double(pixelCount) } 137 | } 138 | } 139 | 140 | // MARK: - 141 | 142 | @Test 143 | func testHistogram() throws { 144 | let red = try Histogram(image: CGImage.withColor(red: 1, green: 0, blue: 0)).peaks 145 | #expect(red.alpha == 1) 146 | #expect(red.red > red.green && red.red > red.blue) 147 | let green = try Histogram(image: CGImage.withColor(red: 0, green: 1, blue: 0)).peaks 148 | #expect(green.alpha == 1) 149 | #expect(green.green > red.red && green.green > red.blue) 150 | let blue = try Histogram(image: CGImage.withColor(red: 0, green: 0, blue: 1)).peaks 151 | #expect(blue.alpha == 1) 152 | #expect(blue.blue > red.red && blue.blue > red.green) 153 | } 154 | 155 | #if canImport(AppKit) 156 | public extension URL { 157 | func revealInFinder() { 158 | NSWorkspace.shared.activateFileViewerSelecting([self]) 159 | } 160 | } 161 | #endif 162 | --------------------------------------------------------------------------------