├── .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 |
--------------------------------------------------------------------------------