diff --git a/metal/.DS_Store b/metal/.DS_Store deleted file mode 100644 index b193254b..00000000 Binary files a/metal/.DS_Store and /dev/null differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Resources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Resources new file mode 120000 index 00000000..953ee36f --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/Resources/Info.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/Resources/Info.plist new file mode 100644 index 00000000..97d32c29 Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/Resources/Info.plist differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/Resources/version.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/Resources/version.plist new file mode 100644 index 00000000..4e2aa793 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/Resources/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 37 + CFBundleShortVersionString + 15.2 + CFBundleVersion + 22516 + ProjectName + XCTest + SourceVersion + 22516000000000000 + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/XCTAutomationSupport b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/XCTAutomationSupport new file mode 100755 index 00000000..e932a9a2 Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/XCTAutomationSupport differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/_CodeSignature/CodeResources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/_CodeSignature/CodeResources new file mode 100644 index 00000000..6242db56 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/A/_CodeSignature/CodeResources @@ -0,0 +1,139 @@ + + + + + files + + Resources/Info.plist + + V9konXwdmzknDqhal6jftIu+1jQ= + + Resources/version.plist + + rEQf7iMtqOPAB33U76v655+WjbU= + + + files2 + + Resources/Info.plist + + hash2 + + O0yvTDqpdWG5rVpmZ3+8jIJ/i6m4HKlD+Ls3zkAzf/8= + + + Resources/version.plist + + hash2 + + vJOS9BonBXQbPOKvvGXjqcW13Rc68etsdnOl9svw0Yw= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/Current b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/Current new file mode 120000 index 00000000..8c7e5a66 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/XCTAutomationSupport b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/XCTAutomationSupport new file mode 120000 index 00000000..01560fe7 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTAutomationSupport.framework/XCTAutomationSupport @@ -0,0 +1 @@ +Versions/Current/XCTAutomationSupport \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Resources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Resources new file mode 120000 index 00000000..953ee36f --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/Resources/Info.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/Resources/Info.plist new file mode 100644 index 00000000..e9497d9d Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/Resources/Info.plist differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/Resources/version.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/Resources/version.plist new file mode 100644 index 00000000..4e2aa793 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/Resources/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 37 + CFBundleShortVersionString + 15.2 + CFBundleVersion + 22516 + ProjectName + XCTest + SourceVersion + 22516000000000000 + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/XCTest b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/XCTest new file mode 100755 index 00000000..765f3efb Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/XCTest differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/_CodeSignature/CodeResources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/_CodeSignature/CodeResources new file mode 100644 index 00000000..10778d96 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/A/_CodeSignature/CodeResources @@ -0,0 +1,139 @@ + + + + + files + + Resources/Info.plist + + e6VnpCjwC64tTaeRTPNDAE73JYU= + + Resources/version.plist + + rEQf7iMtqOPAB33U76v655+WjbU= + + + files2 + + Resources/Info.plist + + hash2 + + W0vratjKR/3Zh0brC+hfS33LNpN40RdxZ52K41Gj8LE= + + + Resources/version.plist + + hash2 + + vJOS9BonBXQbPOKvvGXjqcW13Rc68etsdnOl9svw0Yw= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/Current b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/Current new file mode 120000 index 00000000..8c7e5a66 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/XCTest b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/XCTest new file mode 120000 index 00000000..01f8e3aa --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTest.framework/XCTest @@ -0,0 +1 @@ +Versions/Current/XCTest \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Resources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Resources new file mode 120000 index 00000000..953ee36f --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/Resources/Info.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/Resources/Info.plist new file mode 100644 index 00000000..67c6f890 Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/Resources/Info.plist differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/Resources/version.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/Resources/version.plist new file mode 100644 index 00000000..4e2aa793 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/Resources/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 37 + CFBundleShortVersionString + 15.2 + CFBundleVersion + 22516 + ProjectName + XCTest + SourceVersion + 22516000000000000 + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/XCTestCore b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/XCTestCore new file mode 100755 index 00000000..206ae62b Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/XCTestCore differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/_CodeSignature/CodeResources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/_CodeSignature/CodeResources new file mode 100644 index 00000000..759b1249 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/A/_CodeSignature/CodeResources @@ -0,0 +1,139 @@ + + + + + files + + Resources/Info.plist + + 1GAcVL8DijYMZlpUpjFcsNTKPhs= + + Resources/version.plist + + rEQf7iMtqOPAB33U76v655+WjbU= + + + files2 + + Resources/Info.plist + + hash2 + + nWxLxZXyhQ+2LaeQaosCllwI79pcEUwE+VaNxAQyRlY= + + + Resources/version.plist + + hash2 + + vJOS9BonBXQbPOKvvGXjqcW13Rc68etsdnOl9svw0Yw= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/Current b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/Current new file mode 120000 index 00000000..8c7e5a66 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/XCTestCore b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/XCTestCore new file mode 120000 index 00000000..38994375 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestCore.framework/XCTestCore @@ -0,0 +1 @@ +Versions/Current/XCTestCore \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Resources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Resources new file mode 120000 index 00000000..953ee36f --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/Resources/Info.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/Resources/Info.plist new file mode 100644 index 00000000..c6cc993a Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/Resources/Info.plist differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/Resources/version.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/Resources/version.plist new file mode 100644 index 00000000..4e2aa793 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/Resources/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 37 + CFBundleShortVersionString + 15.2 + CFBundleVersion + 22516 + ProjectName + XCTest + SourceVersion + 22516000000000000 + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/XCTestSupport b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/XCTestSupport new file mode 100755 index 00000000..1dde131f Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/XCTestSupport differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/_CodeSignature/CodeResources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/_CodeSignature/CodeResources new file mode 100644 index 00000000..72982a78 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/A/_CodeSignature/CodeResources @@ -0,0 +1,139 @@ + + + + + files + + Resources/Info.plist + + K7lkeAxIKPyM2ZrakZPcheJDDgo= + + Resources/version.plist + + rEQf7iMtqOPAB33U76v655+WjbU= + + + files2 + + Resources/Info.plist + + hash2 + + 8UotkoAtEwBMeEOIT934XppaCCQAISYTQSoP/Ru/84w= + + + Resources/version.plist + + hash2 + + vJOS9BonBXQbPOKvvGXjqcW13Rc68etsdnOl9svw0Yw= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/Current b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/Current new file mode 120000 index 00000000..8c7e5a66 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/XCTestSupport b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/XCTestSupport new file mode 120000 index 00000000..df476853 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCTestSupport.framework/XCTestSupport @@ -0,0 +1 @@ +Versions/Current/XCTestSupport \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Resources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Resources new file mode 120000 index 00000000..953ee36f --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/Resources/Info.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/Resources/Info.plist new file mode 100644 index 00000000..8ce2629b Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/Resources/Info.plist differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/Resources/version.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/Resources/version.plist new file mode 100644 index 00000000..4e2aa793 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/Resources/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 37 + CFBundleShortVersionString + 15.2 + CFBundleVersion + 22516 + ProjectName + XCTest + SourceVersion + 22516000000000000 + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/XCUIAutomation b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/XCUIAutomation new file mode 100755 index 00000000..512b1d07 Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/XCUIAutomation differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/_CodeSignature/CodeResources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/_CodeSignature/CodeResources new file mode 100644 index 00000000..0ffc7e93 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/A/_CodeSignature/CodeResources @@ -0,0 +1,139 @@ + + + + + files + + Resources/Info.plist + + XpkAsINxmkxk/cJsF3a/dAJ87v8= + + Resources/version.plist + + rEQf7iMtqOPAB33U76v655+WjbU= + + + files2 + + Resources/Info.plist + + hash2 + + 6o3G0l4dEHvAdFaCZ1f1PUEDKehDsaAqU9K/Qdd2Il0= + + + Resources/version.plist + + hash2 + + vJOS9BonBXQbPOKvvGXjqcW13Rc68etsdnOl9svw0Yw= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/Current b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/Current new file mode 120000 index 00000000..8c7e5a66 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/XCUIAutomation b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/XCUIAutomation new file mode 120000 index 00000000..ef60c6ee --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUIAutomation.framework/XCUIAutomation @@ -0,0 +1 @@ +Versions/Current/XCUIAutomation \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Resources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Resources new file mode 120000 index 00000000..953ee36f --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/Resources/Info.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/Resources/Info.plist new file mode 100644 index 00000000..fbe50423 Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/Resources/Info.plist differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/Resources/version.plist b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/Resources/version.plist new file mode 100644 index 00000000..4e2aa793 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/Resources/version.plist @@ -0,0 +1,16 @@ + + + + + BuildVersion + 37 + CFBundleShortVersionString + 15.2 + CFBundleVersion + 22516 + ProjectName + XCTest + SourceVersion + 22516000000000000 + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/XCUnit b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/XCUnit new file mode 100755 index 00000000..dd280d25 Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/XCUnit differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/_CodeSignature/CodeResources b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/_CodeSignature/CodeResources new file mode 100644 index 00000000..3736e081 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/A/_CodeSignature/CodeResources @@ -0,0 +1,139 @@ + + + + + files + + Resources/Info.plist + + HE8FiPIPKPDWW7iL2oVuMGEv4aY= + + Resources/version.plist + + rEQf7iMtqOPAB33U76v655+WjbU= + + + files2 + + Resources/Info.plist + + hash2 + + E/S5yrf4TLTREBU5hYXVSxoB71N0hSmI953LpOY0+0g= + + + Resources/version.plist + + hash2 + + vJOS9BonBXQbPOKvvGXjqcW13Rc68etsdnOl9svw0Yw= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/Current b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/Current new file mode 120000 index 00000000..8c7e5a66 --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/XCUnit b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/XCUnit new file mode 120000 index 00000000..306bc9bc --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/Frameworks/XCUnit.framework/XCUnit @@ -0,0 +1 @@ +Versions/Current/XCUnit \ No newline at end of file diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/libXCTestBundleInject.dylib b/metal/binary/stable/mglMetal.app/Contents/Frameworks/libXCTestBundleInject.dylib new file mode 100755 index 00000000..5677986c Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/libXCTestBundleInject.dylib differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Frameworks/libXCTestSwiftSupport.dylib b/metal/binary/stable/mglMetal.app/Contents/Frameworks/libXCTestSwiftSupport.dylib new file mode 100755 index 00000000..0b111030 Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/Frameworks/libXCTestSwiftSupport.dylib differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Info.plist b/metal/binary/stable/mglMetal.app/Contents/Info.plist index 1fd41474..8af047b1 100644 --- a/metal/binary/stable/mglMetal.app/Contents/Info.plist +++ b/metal/binary/stable/mglMetal.app/Contents/Info.plist @@ -41,9 +41,9 @@ DTSDKName macosx14.2 DTXcode - 1510 + 1520 DTXcodeBuild - 15C65 + 15C500b LSMinimumSystemVersion 10.15 NSHumanReadableCopyright diff --git a/metal/binary/stable/mglMetal.app/Contents/MacOS/mglMetal b/metal/binary/stable/mglMetal.app/Contents/MacOS/mglMetal index bb8439e8..2e4c2b06 100755 Binary files a/metal/binary/stable/mglMetal.app/Contents/MacOS/mglMetal and b/metal/binary/stable/mglMetal.app/Contents/MacOS/mglMetal differ diff --git a/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/Frameworks/libswift_Concurrency.dylib b/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/Frameworks/libswift_Concurrency.dylib new file mode 100755 index 00000000..6923a0da Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/Frameworks/libswift_Concurrency.dylib differ diff --git a/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/Info.plist b/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/Info.plist new file mode 100644 index 00000000..20e2a5ee --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/Info.plist @@ -0,0 +1,46 @@ + + + + + BuildMachineOSBuild + 23C71 + CFBundleDevelopmentRegion + en + CFBundleExecutable + mglMetalTests + CFBundleIdentifier + gru.mglMetalTests + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + mglMetalTests + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + + DTPlatformName + macosx + DTPlatformVersion + 14.2 + DTSDKBuild + 23C53 + DTSDKName + macosx14.2 + DTXcode + 1520 + DTXcodeBuild + 15C500b + LSMinimumSystemVersion + 10.15 + + diff --git a/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/MacOS/mglMetalTests b/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/MacOS/mglMetalTests new file mode 100755 index 00000000..1eda79d1 Binary files /dev/null and b/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/MacOS/mglMetalTests differ diff --git a/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/_CodeSignature/CodeResources b/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/_CodeSignature/CodeResources new file mode 100644 index 00000000..66d7956f --- /dev/null +++ b/metal/binary/stable/mglMetal.app/Contents/PlugIns/mglMetalTests.xctest/Contents/_CodeSignature/CodeResources @@ -0,0 +1,125 @@ + + + + + files + + files2 + + Frameworks/libswift_Concurrency.dylib + + cdhash + + lMyxZwOuFohh+Fcknez4IbkEo/A= + + requirement + cdhash H"94ccb16703ae168861f857249decf821b904a3f0" or cdhash H"2df8f77ca131cd10508992f453212709099ea525" + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/metal/binary/stable/mglMetal.app/Contents/Resources/Assets.car b/metal/binary/stable/mglMetal.app/Contents/Resources/Assets.car index 95b680d4..b676b97a 100644 Binary files a/metal/binary/stable/mglMetal.app/Contents/Resources/Assets.car and b/metal/binary/stable/mglMetal.app/Contents/Resources/Assets.car differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist b/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist index 00b88726..8d5070f3 100644 Binary files a/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist and b/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib b/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib index cd932efc..ea98b368 100644 Binary files a/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib and b/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib b/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib index dc54bb49..bab15da6 100644 Binary files a/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib and b/metal/binary/stable/mglMetal.app/Contents/Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib differ diff --git a/metal/binary/stable/mglMetal.app/Contents/Resources/default.metallib b/metal/binary/stable/mglMetal.app/Contents/Resources/default.metallib index 464b2739..ad45903e 100644 Binary files a/metal/binary/stable/mglMetal.app/Contents/Resources/default.metallib and b/metal/binary/stable/mglMetal.app/Contents/Resources/default.metallib differ diff --git a/metal/binary/stable/mglMetal.app/Contents/_CodeSignature/CodeResources b/metal/binary/stable/mglMetal.app/Contents/_CodeSignature/CodeResources index d28b95e6..f9f7d640 100644 --- a/metal/binary/stable/mglMetal.app/Contents/_CodeSignature/CodeResources +++ b/metal/binary/stable/mglMetal.app/Contents/_CodeSignature/CodeResources @@ -10,19 +10,19 @@ Resources/Assets.car - dtDz31nknNX9dsEMzXw4Hy5JF78= + hTi/zigWRpQaVfOihzyWgLH0xHs= Resources/Base.lproj/Main.storyboardc/Info.plist - 8oAe3mSzWEjJvzhmChXwFah7Tmg= + faGazw0Vz3bvZDBbsKiq4rThd/8= Resources/Base.lproj/Main.storyboardc/MainMenu.nib - odyBtpvZQaAG9j4LU7a2L2DubMA= + Q6Jyzc/a9VRndQdtO9hKS9Ol2YQ= Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib - Vz4dpsRHJWIUPhGE+Jn7NgYB7Zc= + Iy/4ow544v4qggd1RKNFU+rlMSI= Resources/Base.lproj/Main.storyboardc/XfG-lQ-9wD-view-m2S-Jp-Qdl.nib @@ -30,11 +30,92 @@ Resources/default.metallib - /7Beoy0HKDnO8G4zmS19szH9vWA= + V3xdYEOgTN+db6jnntMz1y+/4XU= files2 + Frameworks/XCTAutomationSupport.framework + + cdhash + + qgovFnU3KbwKbtkhooXMhSUwo3s= + + requirement + cdhash H"aa0a2f16753729bc0a6ed921a285cc852530a37b" or cdhash H"fb0210f4c0d8ba8bf088c670b977f2f5c8438ded" or cdhash H"f96c9f8d2160b4dd97441493eb5bc2d13081ddb2" + + Frameworks/XCTest.framework + + cdhash + + KN98B4j2hKrLzeAqJb9eJGK9Ltk= + + requirement + cdhash H"28df7c0788f684aacbcde02a25bf5e2462bd2ed9" or cdhash H"4d78101f1811dae640d22daf94abaa96f3c60337" or cdhash H"ab4c6fdda9ad024aed4a98cbf9139ba97dbb4ea2" + + Frameworks/XCTestCore.framework + + cdhash + + VTIJYlQyXJIhNtAUxz5e6N9V7ls= + + requirement + cdhash H"5532096254325c922136d014c73e5ee8df55ee5b" or cdhash H"4712b72906bb27732ccd8ab8d4304f3160d7d194" or cdhash H"f773018a3cf9a7d62420a3c8ed2c7c4759b40bf1" + + Frameworks/XCTestSupport.framework + + cdhash + + 4+zxRj4O1jVmG60yP+BIi1Abg/Q= + + requirement + cdhash H"e3ecf1463e0ed635661bad323fe0488b501b83f4" or cdhash H"d813a821f4910d0af8b3b8ace76d3ba62f967235" or cdhash H"6b4ae86236e2bae271b2ca7d486df16e0e7ad5a1" + + Frameworks/XCUIAutomation.framework + + cdhash + + YDBNVnnINmULxr6CLYxVZkyPCug= + + requirement + cdhash H"60304d5679c836650bc6be822d8c55664c8f0ae8" or cdhash H"12cc148ee11095233e17f3f48d6ea525845b6974" or cdhash H"ffe1ff55b94e73fdb7aa96f3738036b4c8f0adc2" + + Frameworks/XCUnit.framework + + cdhash + + RBql2Srp/FXcCKLA6RL9Q0JeuBQ= + + requirement + cdhash H"441aa5d92ae9fc55dc08a2c0e912fd43425eb814" or cdhash H"6b1df3b82854ae49c2bdc6857c079412d0ece15f" or cdhash H"d0df8e7f80e58eff688a65c290daab21df5836a3" + + Frameworks/libXCTestBundleInject.dylib + + cdhash + + BT7ErMZOkKXM3lehI7IHRHH6sH8= + + requirement + cdhash H"053ec4acc64e90a5ccde57a123b2074471fab07f" or cdhash H"0c8a325aa63e5e82e227702f525e9a7b58e8b239" or cdhash H"f9d9df69215053abf3ea4d212ad23b92349a3a2a" + + Frameworks/libXCTestSwiftSupport.dylib + + cdhash + + /8YqEkFQTfmiIHvYw1In61OjO0I= + + requirement + cdhash H"ffc62a1241504df9a2207bd8c35227eb53a33b42" or cdhash H"c416302d79ba6a641fad30c9eb432f6d724a8b9b" or cdhash H"9d13f59fb20a3c26993120b338ea915fc7040c4c" + + PlugIns/mglMetalTests.xctest + + cdhash + + bhJ98AI2XcZQfiiZ3DK+4pyDnhE= + + requirement + cdhash H"6e127df002365dc6507e2899dc32bee29c839e11" + Resources/AppIcon.icns hash2 @@ -46,28 +127,28 @@ hash2 - wRV3FMaC3U1BnpDXUJu9sd+YwAfOaCgM5eWirkZVWlg= + M3lOA2tf7VZ6W1KJ/oGCtmnZmixtS/Z2063CLiE4SGk= Resources/Base.lproj/Main.storyboardc/Info.plist hash2 - R9hgvcQZ+DN7BWgjShaq1xhm6+DDtB0vPNAzyVtnR8o= + VHe3fWzDAjNp7FyC65k3mTUfvtWeD6qVjRv74g2dXbo= Resources/Base.lproj/Main.storyboardc/MainMenu.nib hash2 - xbKdtAE4TsZlqR/vUszMZntopubQUk0S3Kt8IBNK8dQ= + Jh8CSrxbbmxB/q4y5mSvWO6CfPYspL+sJfGhN579Bzo= Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib hash2 - vfu+JdnlxlDispVDFZ8pS6LzP1W5hnILhHm9hwrTKAs= + oMk6erkkXG0/2vWjKI7KH2qN9jiwE4aBFYforx4jMRU= Resources/Base.lproj/Main.storyboardc/XfG-lQ-9wD-view-m2S-Jp-Qdl.nib @@ -81,7 +162,7 @@ hash2 - 3LF69lZBqC6bMBF6mZzzspgTrGnEc5co7y5AjsEsbsg= + n0qoeahStIZhme5UoaUg0fGPV26404WkjBHn/9qdC2E= diff --git a/metal/command-model-etc.png b/metal/command-model-etc.png new file mode 100644 index 00000000..3d60b2f2 Binary files /dev/null and b/metal/command-model-etc.png differ diff --git a/metal/drawabes.md b/metal/drawabes.md new file mode 100644 index 00000000..19d1ffab --- /dev/null +++ b/metal/drawabes.md @@ -0,0 +1,115 @@ +# A few points about drawables + +Here are some notes about Metal / macOS [drawables](https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html). + +# What's a drawable? + +A [drawable]([url](https://developer.apple.com/documentation/metal/mtldrawable)) is a system resource that we need to acquire before we can put rendering results on the display. +I (BSH) think of drawables as a combination of: + + - a Metal texture that can receive our rendering results + - other, hidden config and resources that the system needs for scheduling WRT the display refresh cycle and data transfer to the display + +We don't implement or manage drawables ourselves. +The system creates them and manages them in a limited pool. +We request a drawable when we're ready to render a frame and release the drawable once we're done with each frame. +Each drawable is good for one frame only, then the system needs it back for recycling before we can request it again for a future frame. +If we request too many drawables at once, our app will block, waiting for one to be released and recycled. + +# Drawable timing + +In Mgl Metal we're keeping track of two timestamps per frame that are related to drawables: + + - `drawableAcquired` is a CPU "get secs" timestamps that we take right after requesting and acquiring the drawable before rendering on a frame. This can tell us if we're blocking unexpectedly, waiting for the drawable, perhaps for performance reasons like low graphics memory. + - `drawablePresented` is a CPU timestamp [reported to us by the system]([url](https://developer.apple.com/documentation/metal/mtldrawable/2806855-presentedtime)https://developer.apple.com/documentation/metal/mtldrawable/2806855-presentedtime). This may be our best estimate of frame timing, although we don't have access to how the system records it. + +Here's a simplified sequence of events to illustrate how these timestamps line up. + +| frame 0 | frame 1 | frame 1 | +| --- | --- | --- | +| begin `draw()` | | | +| request drawable | | | +| measure `drawableAcquired` | | | +| encode render commands for drawable | | | +| request drawable presentation | | | +| return from `draw()` | | | +| ... | | | +| system presents drawable | | | +| system calls our `presentationHandler` | | | +| read `drawablePresented` | | | +| ... | | | +| | begin `draw()` | | +| | report `drawablePresented` for **frame 0** | | +| | request drawable | | +| | measure `drawableAcquired` | | +| | encode render commands for drawable | | +| | request drawable presentation | | +| | return from `draw()` | | +| | ... | | +| | system presents drawable | | +| | system calls our `presentationHandler` | | +| | read `drawablePresented` | | +| | ... | | +| | | begin `draw()` | +| | | report `drawablePresented` for **frame 1** | +| | | request drawable | +| | | ... | + +A lot of this happens asynchrononously, via callbacks from the system to our Mgl Metal code. Specifically: + + - The system calls our `draw()` callback when it wants us to start working on the next frame, on a system-managed schedule ([CADisplayLink](https://developer.apple.com/documentation/quartzcore/cadisplaylink)). + - The system calls our `presentationHandler` some time later, after a frame has been presented on the display. + - The `draw()` and `presentationHandler` schedules both must be tied to the same display and the callbacks tend to alternate, but seem not to be explicitly synchronized. + +# Triple buffering vs one-flush-at-a-time + +Apple recommends "triple buffering" as a [best practice]([url](https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html)https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html). +By this they mean apps should let the system manage a pool of 3 drawables, and synchronize these to a corresponding pool of data buffers that the app manipulates. +In this way custom app code running on the CPU can be working up to 2 frames ahead of the GPU, preparing data ahead of time and keeping the GPU as busy as possible. +This approach is good for maximizing throughput and for not dropping frames while waiting for irregular CPU tasks to complete. +This approach also adds some implementation complexity and lag of up to 2 frames between what the CPU might consider "now" vs what's appearing on the display. + +Mgl Metal isn't doing this, currently. + +Our current model for client code expects to issue drawing commands, issue a flush command, and wait up to one refresh interval for the flush command to complete. +If a flush command isn't presented, and therefore isn't complete, until 2 frames later, this would mean blocking the client for a long time and dropping the intermediate frames. +Keeping the client unblocked and sending drawing commands, while also waiting for 1-2 incomplete frames in flight, would require a different design for the client code. + +Our current compromise is to send a flush, and wait up to one refresh interval for the next `draw()` callback, and then unblock the client. +As a consequence, the `drawablePresented` time we see reported on the flush for **frame 1** is really whatever timestamp we read most recently, for **frame 0**. +This allows us to make use of the system-reported `drawablePresented` timestamps, and keep existing client code as-is. +It also means we need to shift `drawablePresented` timestamps by one frame when interpreting command results. + +# Double buffering + +It should be possible to configure the Mgl Metal app to use 2, rather than 3 drawables ([2 and 3 are the only options](https://developer.apple.com/documentation/quartzcore/cametallayer/2938720-maximumdrawablecount)). +Currently we're using the default of 3. +For now, we are assuming that having an extra drawable sitting around in the system's pool is not harmful. +One way this might prove false is if we are somehow, accidentally, asking the system to present more than 2 drawables at once, and if these then get queued up ahead of the CPU, as in Apple's triple buffering example. + +If we start seeing trouble with the current one-flush-at-a-time compromise, we could investigate whether using 2 drawables has advantages for us. + +# Triple buffering in batch mode + +We recently enhanced Mgl Metal to support a "batch mode" where multiple drawing and flush commands can be enqueued ahead of time, then processed by `draw()` as fast as possible. +The inspiration for this was to decouple client communication work, which is potentally slow and introduces timing jutter, from rendering work, which we want to be steady and solid. + +Batch mode might have an added benefit of decoupling frame presentations from client flush calls. +We might be free to adopt the recommended triple buffering approach, and to associate system reported `drawablePresented` timestamps with the corresponding flush commands, even if these are known 2 frames after the fact. +We'd be free to do this because in batch mode the client would not be blocked waiting for a reply to the most recent flush command. + +If we start seeing trouble with stimuli dropping frames, even in batch mode, then we could consider reworking the `drawablePresented` bookkeeping to allow multiple flushes in flight at once. + +# When to request the drawable + +Apple's drawables [best practice](https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html) guidance encourages us to hold each drawable for the shortest time possible. +Currently Mgl Metal is doing pretty well at this, by distinguising between drawing vs non-drawing commands, and only requesting a drawable when it sees a drawing command. +However, we are still holding the drawable while processing potentially multiple drawing commands, until we get a flush command. + +We could potentially tighten this a notch further by rendering drawing commands to an offscreen texture, instead of directly to the onscreen drawable's texture. +We could process muliple drawing commands and render offscreen without requesting a drawable at all. +Then, when we get a flush command, we could finally request the drawable and set up a short render pass to blit the offscreen texture to the drawable's texture. +For complicated frames with many or slow drawing commands, this could reduce the time we spend holding the drawable. + +If we start seeing trouble with many or slow drawing commands, we could consider reworking our rendering pipeline to work offscreen as much as possible. +One side-benefit of this approach would be to make "screen grab" as easy as reading the main offscreen texture. diff --git a/metal/mglMetal.xcodeproj/project.pbxproj b/metal/mglMetal.xcodeproj/project.pbxproj index b9fffddd..346224c5 100644 --- a/metal/mglMetal.xcodeproj/project.pbxproj +++ b/metal/mglMetal.xcodeproj/project.pbxproj @@ -11,9 +11,46 @@ 4D56190B27628883009AB2E2 /* mglLocalServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D56190A27628883009AB2E2 /* mglLocalServer.swift */; }; 4D56190D27629A39009AB2E2 /* mglLocalClientServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D56190C27629A39009AB2E2 /* mglLocalClientServerTests.swift */; }; 4D5619112762A8F4009AB2E2 /* mglLocalClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5619102762A8F4009AB2E2 /* mglLocalClient.swift */; }; + 4D5782C92B4722110050EF81 /* mglInfoCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782C82B4722110050EF81 /* mglInfoCommand.swift */; }; + 4D5782CB2B472AAE0050EF81 /* mglLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782CA2B472AAE0050EF81 /* mglLogger.swift */; }; + 4D5782CD2B4748530050EF81 /* mglGetErrorMessageCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782CC2B4748530050EF81 /* mglGetErrorMessageCommand.swift */; }; + 4D5782CF2B474BA60050EF81 /* mglFrameGrabCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782CE2B474BA60050EF81 /* mglFrameGrabCommand.swift */; }; + 4D5782D12B474DA30050EF81 /* mglMinimizeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782D02B474DA30050EF81 /* mglMinimizeCommand.swift */; }; + 4D5782D32B474E820050EF81 /* mglBltTextureCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782D22B474E820050EF81 /* mglBltTextureCommand.swift */; }; + 4D5782D52B4752D00050EF81 /* mglDotsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782D42B4752D00050EF81 /* mglDotsCommand.swift */; }; + 4D5782D72B4753AC0050EF81 /* mglLineCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782D62B4753AC0050EF81 /* mglLineCommand.swift */; }; + 4D5782D92B47547C0050EF81 /* mglPolygonCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782D82B47547C0050EF81 /* mglPolygonCommand.swift */; }; + 4D5782DB2B4755010050EF81 /* mglArcsConmmand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782DA2B4755010050EF81 /* mglArcsConmmand.swift */; }; + 4D5782DD2B4757520050EF81 /* mglUpdateTextureCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782DC2B4757520050EF81 /* mglUpdateTextureCommand.swift */; }; + 4D5782DF2B4761350050EF81 /* mglRepeatFlickerCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782DE2B4761350050EF81 /* mglRepeatFlickerCommand.swift */; }; + 4D5782E12B4764F60050EF81 /* mglRepeatBltsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782E02B4764F60050EF81 /* mglRepeatBltsCommand.swift */; }; + 4D5782E32B4767730050EF81 /* mglRepeatQuadsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782E22B4767730050EF81 /* mglRepeatQuadsCommand.swift */; }; + 4D5782E52B47693D0050EF81 /* mglRepeatDotsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782E42B47693D0050EF81 /* mglRepeatDotsCommand.swift */; }; + 4D5782E72B476A370050EF81 /* mglRepeatFlushCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5782E62B476A370050EF81 /* mglRepeatFlushCommand.swift */; }; 4D5882B2285A89FD005DDF00 /* mglCommandModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5882B1285A89FD005DDF00 /* mglCommandModel.swift */; }; + 4D5F729D2B45B07600B4DA29 /* mglCreateTextureCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F729C2B45B07600B4DA29 /* mglCreateTextureCommand.swift */; }; + 4D5F729F2B45B6C000B4DA29 /* mglSetRenderTargetCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F729E2B45B6C000B4DA29 /* mglSetRenderTargetCommand.swift */; }; + 4D5F72A32B45CEFA00B4DA29 /* mglReadTextureCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72A22B45CEFA00B4DA29 /* mglReadTextureCommand.swift */; }; + 4D5F72A52B45D25900B4DA29 /* mglDeleteTextureCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72A42B45D25900B4DA29 /* mglDeleteTextureCommand.swift */; }; + 4D5F72A72B45D53500B4DA29 /* mglPingCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72A62B45D53500B4DA29 /* mglPingCommand.swift */; }; + 4D5F72A92B45D60B00B4DA29 /* mglDrainSystemEventsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72A82B45D60B00B4DA29 /* mglDrainSystemEventsCommand.swift */; }; + 4D5F72AB2B45D75300B4DA29 /* mglFullscreenCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72AA2B45D75300B4DA29 /* mglFullscreenCommand.swift */; }; + 4D5F72AD2B45D8BD00B4DA29 /* mglWindowedCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72AC2B45D8BD00B4DA29 /* mglWindowedCommand.swift */; }; + 4D5F72AF2B45D93500B4DA29 /* mglSetWindowFrameInDisplayCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72AE2B45D93500B4DA29 /* mglSetWindowFrameInDisplayCommand.swift */; }; + 4D5F72B12B45DAE800B4DA29 /* mglGetWindowFrameInDisplayCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72B02B45DAE800B4DA29 /* mglGetWindowFrameInDisplayCommand.swift */; }; + 4D5F72B32B45DD9B00B4DA29 /* mglSetViewColorPixelFormatCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72B22B45DD9B00B4DA29 /* mglSetViewColorPixelFormatCommand.swift */; }; + 4D5F72B52B45E0A500B4DA29 /* mglDisplayCursorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72B42B45E0A400B4DA29 /* mglDisplayCursorCommand.swift */; }; + 4D5F72B72B45E24F00B4DA29 /* mglSetXformCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72B62B45E24F00B4DA29 /* mglSetXformCommand.swift */; }; + 4D5F72B92B45E4D500B4DA29 /* mglStartStencilCreationCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72B82B45E4D500B4DA29 /* mglStartStencilCreationCommand.swift */; }; + 4D5F72BB2B45E5DF00B4DA29 /* mglFinishStencilCreationCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72BA2B45E5DE00B4DA29 /* mglFinishStencilCreationCommand.swift */; }; + 4D5F72BD2B45E62900B4DA29 /* mglSelectStencilCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72BC2B45E62900B4DA29 /* mglSelectStencilCommand.swift */; }; + 4D5F72BF2B45E87600B4DA29 /* mglQuadCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D5F72BE2B45E87600B4DA29 /* mglQuadCommand.swift */; }; + 4D755E062B6941FB00ACB083 /* mglSampleTimestampsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D755E052B6941FB00ACB083 /* mglSampleTimestampsCommand.swift */; }; 4D8B32812824627800278B6F /* mglDepthStencilConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8B32802824627800278B6F /* mglDepthStencilConfig.swift */; }; 4DA4671F2763CB8E00B65B9F /* mglServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DA4671E2763CB8E00B65B9F /* mglServer.swift */; }; + 4DCD58472B3615BF00E08547 /* mglFlushCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCD58462B3615BF00E08547 /* mglFlushCommand.swift */; }; + 4DCD58492B36178E00E08547 /* mglSetClearColorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCD58482B36178E00E08547 /* mglSetClearColorCommand.swift */; }; + 4DCD584B2B36335500E08547 /* mglRenderer2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCD584A2B36335500E08547 /* mglRenderer2.swift */; }; D08D4BBE23B8834C00A27C79 /* mglCommandInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08D4BBD23B8834C00A27C79 /* mglCommandInterface.swift */; }; D0A59C7A23C2E41900D3AE93 /* mglSecs.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A59C7923C2E41900D3AE93 /* mglSecs.m */; }; D0F6B96E23B6D9E700B45409 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F6B96D23B6D9E700B45409 /* AppDelegate.swift */; }; @@ -22,7 +59,6 @@ D0F6B97523B6D9E800B45409 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D0F6B97323B6D9E800B45409 /* Main.storyboard */; }; D0F6B98123B6D9E800B45409 /* mglMetalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F6B98023B6D9E800B45409 /* mglMetalTests.swift */; }; D0F6B98C23B6D9E800B45409 /* mglMetalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F6B98B23B6D9E800B45409 /* mglMetalUITests.swift */; }; - D0F6B99A23B6DAD600B45409 /* mglRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F6B99923B6DAD600B45409 /* mglRenderer.swift */; }; D0F6B9A223B6ED4300B45409 /* mglPrimitives.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F6B9A123B6ED4300B45409 /* mglPrimitives.swift */; }; D0F6B9A423B6EDE600B45409 /* mglShaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = D0F6B9A323B6EDE600B45409 /* mglShaders.metal */; }; /* End PBXBuildFile section */ @@ -49,10 +85,47 @@ 4D56190A27628883009AB2E2 /* mglLocalServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglLocalServer.swift; sourceTree = ""; }; 4D56190C27629A39009AB2E2 /* mglLocalClientServerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglLocalClientServerTests.swift; sourceTree = ""; }; 4D5619102762A8F4009AB2E2 /* mglLocalClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglLocalClient.swift; sourceTree = ""; }; + 4D5782C82B4722110050EF81 /* mglInfoCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglInfoCommand.swift; sourceTree = ""; }; + 4D5782CA2B472AAE0050EF81 /* mglLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglLogger.swift; sourceTree = ""; }; + 4D5782CC2B4748530050EF81 /* mglGetErrorMessageCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglGetErrorMessageCommand.swift; sourceTree = ""; }; + 4D5782CE2B474BA60050EF81 /* mglFrameGrabCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglFrameGrabCommand.swift; sourceTree = ""; }; + 4D5782D02B474DA30050EF81 /* mglMinimizeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglMinimizeCommand.swift; sourceTree = ""; }; + 4D5782D22B474E820050EF81 /* mglBltTextureCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglBltTextureCommand.swift; sourceTree = ""; }; + 4D5782D42B4752D00050EF81 /* mglDotsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglDotsCommand.swift; sourceTree = ""; }; + 4D5782D62B4753AC0050EF81 /* mglLineCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglLineCommand.swift; sourceTree = ""; }; + 4D5782D82B47547C0050EF81 /* mglPolygonCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglPolygonCommand.swift; sourceTree = ""; }; + 4D5782DA2B4755010050EF81 /* mglArcsConmmand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglArcsConmmand.swift; sourceTree = ""; }; + 4D5782DC2B4757520050EF81 /* mglUpdateTextureCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglUpdateTextureCommand.swift; sourceTree = ""; }; + 4D5782DE2B4761350050EF81 /* mglRepeatFlickerCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglRepeatFlickerCommand.swift; sourceTree = ""; }; + 4D5782E02B4764F60050EF81 /* mglRepeatBltsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglRepeatBltsCommand.swift; sourceTree = ""; }; + 4D5782E22B4767730050EF81 /* mglRepeatQuadsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglRepeatQuadsCommand.swift; sourceTree = ""; }; + 4D5782E42B47693D0050EF81 /* mglRepeatDotsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglRepeatDotsCommand.swift; sourceTree = ""; }; + 4D5782E62B476A370050EF81 /* mglRepeatFlushCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglRepeatFlushCommand.swift; sourceTree = ""; }; 4D5882B1285A89FD005DDF00 /* mglCommandModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglCommandModel.swift; sourceTree = ""; }; + 4D5F729C2B45B07600B4DA29 /* mglCreateTextureCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglCreateTextureCommand.swift; sourceTree = ""; }; + 4D5F729E2B45B6C000B4DA29 /* mglSetRenderTargetCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglSetRenderTargetCommand.swift; sourceTree = ""; }; + 4D5F72A22B45CEFA00B4DA29 /* mglReadTextureCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglReadTextureCommand.swift; sourceTree = ""; }; + 4D5F72A42B45D25900B4DA29 /* mglDeleteTextureCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglDeleteTextureCommand.swift; sourceTree = ""; }; + 4D5F72A62B45D53500B4DA29 /* mglPingCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglPingCommand.swift; sourceTree = ""; }; + 4D5F72A82B45D60B00B4DA29 /* mglDrainSystemEventsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglDrainSystemEventsCommand.swift; sourceTree = ""; }; + 4D5F72AA2B45D75300B4DA29 /* mglFullscreenCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglFullscreenCommand.swift; sourceTree = ""; }; + 4D5F72AC2B45D8BD00B4DA29 /* mglWindowedCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglWindowedCommand.swift; sourceTree = ""; }; + 4D5F72AE2B45D93500B4DA29 /* mglSetWindowFrameInDisplayCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglSetWindowFrameInDisplayCommand.swift; sourceTree = ""; }; + 4D5F72B02B45DAE800B4DA29 /* mglGetWindowFrameInDisplayCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglGetWindowFrameInDisplayCommand.swift; sourceTree = ""; }; + 4D5F72B22B45DD9B00B4DA29 /* mglSetViewColorPixelFormatCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglSetViewColorPixelFormatCommand.swift; sourceTree = ""; }; + 4D5F72B42B45E0A400B4DA29 /* mglDisplayCursorCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglDisplayCursorCommand.swift; sourceTree = ""; }; + 4D5F72B62B45E24F00B4DA29 /* mglSetXformCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglSetXformCommand.swift; sourceTree = ""; }; + 4D5F72B82B45E4D500B4DA29 /* mglStartStencilCreationCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglStartStencilCreationCommand.swift; sourceTree = ""; }; + 4D5F72BA2B45E5DE00B4DA29 /* mglFinishStencilCreationCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglFinishStencilCreationCommand.swift; sourceTree = ""; }; + 4D5F72BC2B45E62900B4DA29 /* mglSelectStencilCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglSelectStencilCommand.swift; sourceTree = ""; }; + 4D5F72BE2B45E87600B4DA29 /* mglQuadCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglQuadCommand.swift; sourceTree = ""; }; + 4D755E052B6941FB00ACB083 /* mglSampleTimestampsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglSampleTimestampsCommand.swift; sourceTree = ""; }; 4D8B32802824627800278B6F /* mglDepthStencilConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglDepthStencilConfig.swift; sourceTree = ""; }; 4DA4671B2763C16200B65B9F /* mglCommandTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = mglCommandTypes.h; sourceTree = ""; }; 4DA4671E2763CB8E00B65B9F /* mglServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglServer.swift; sourceTree = ""; }; + 4DCD58462B3615BF00E08547 /* mglFlushCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglFlushCommand.swift; sourceTree = ""; }; + 4DCD58482B36178E00E08547 /* mglSetClearColorCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglSetClearColorCommand.swift; sourceTree = ""; }; + 4DCD584A2B36335500E08547 /* mglRenderer2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglRenderer2.swift; sourceTree = ""; }; D08D4BBD23B8834C00A27C79 /* mglCommandInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglCommandInterface.swift; sourceTree = ""; }; D0A59C7823C2E41900D3AE93 /* mglSecs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = mglSecs.h; sourceTree = ""; }; D0A59C7923C2E41900D3AE93 /* mglSecs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = mglSecs.m; sourceTree = ""; }; @@ -69,7 +142,6 @@ D0F6B98723B6D9E800B45409 /* mglMetalUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = mglMetalUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D0F6B98B23B6D9E800B45409 /* mglMetalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglMetalUITests.swift; sourceTree = ""; }; D0F6B98D23B6D9E800B45409 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D0F6B99923B6DAD600B45409 /* mglRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglRenderer.swift; sourceTree = ""; }; D0F6B99D23B6DB3A00B45409 /* mglMetal-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "mglMetal-Bridging-Header.h"; sourceTree = ""; }; D0F6B9A123B6ED4300B45409 /* mglPrimitives.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mglPrimitives.swift; sourceTree = ""; }; D0F6B9A323B6EDE600B45409 /* mglShaders.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; lineEnding = 0; path = mglShaders.metal; sourceTree = ""; }; @@ -100,6 +172,48 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4DCD58452B36159300E08547 /* commands */ = { + isa = PBXGroup; + children = ( + 4DCD58462B3615BF00E08547 /* mglFlushCommand.swift */, + 4DCD58482B36178E00E08547 /* mglSetClearColorCommand.swift */, + 4D5F729C2B45B07600B4DA29 /* mglCreateTextureCommand.swift */, + 4D5F729E2B45B6C000B4DA29 /* mglSetRenderTargetCommand.swift */, + 4D5F72A22B45CEFA00B4DA29 /* mglReadTextureCommand.swift */, + 4D5F72A42B45D25900B4DA29 /* mglDeleteTextureCommand.swift */, + 4D5F72A62B45D53500B4DA29 /* mglPingCommand.swift */, + 4D5F72A82B45D60B00B4DA29 /* mglDrainSystemEventsCommand.swift */, + 4D5F72AA2B45D75300B4DA29 /* mglFullscreenCommand.swift */, + 4D5F72AC2B45D8BD00B4DA29 /* mglWindowedCommand.swift */, + 4D5F72AE2B45D93500B4DA29 /* mglSetWindowFrameInDisplayCommand.swift */, + 4D5F72B02B45DAE800B4DA29 /* mglGetWindowFrameInDisplayCommand.swift */, + 4D5F72B22B45DD9B00B4DA29 /* mglSetViewColorPixelFormatCommand.swift */, + 4D5F72B42B45E0A400B4DA29 /* mglDisplayCursorCommand.swift */, + 4D5F72B62B45E24F00B4DA29 /* mglSetXformCommand.swift */, + 4D5F72B82B45E4D500B4DA29 /* mglStartStencilCreationCommand.swift */, + 4D5F72BA2B45E5DE00B4DA29 /* mglFinishStencilCreationCommand.swift */, + 4D5F72BC2B45E62900B4DA29 /* mglSelectStencilCommand.swift */, + 4D5F72BE2B45E87600B4DA29 /* mglQuadCommand.swift */, + 4D5782C82B4722110050EF81 /* mglInfoCommand.swift */, + 4D5782CC2B4748530050EF81 /* mglGetErrorMessageCommand.swift */, + 4D5782CE2B474BA60050EF81 /* mglFrameGrabCommand.swift */, + 4D5782D02B474DA30050EF81 /* mglMinimizeCommand.swift */, + 4D5782D22B474E820050EF81 /* mglBltTextureCommand.swift */, + 4D5782D42B4752D00050EF81 /* mglDotsCommand.swift */, + 4D5782D62B4753AC0050EF81 /* mglLineCommand.swift */, + 4D5782D82B47547C0050EF81 /* mglPolygonCommand.swift */, + 4D5782DA2B4755010050EF81 /* mglArcsConmmand.swift */, + 4D5782DC2B4757520050EF81 /* mglUpdateTextureCommand.swift */, + 4D5782DE2B4761350050EF81 /* mglRepeatFlickerCommand.swift */, + 4D5782E02B4764F60050EF81 /* mglRepeatBltsCommand.swift */, + 4D5782E22B4767730050EF81 /* mglRepeatQuadsCommand.swift */, + 4D5782E42B47693D0050EF81 /* mglRepeatDotsCommand.swift */, + 4D5782E62B476A370050EF81 /* mglRepeatFlushCommand.swift */, + 4D755E052B6941FB00ACB083 /* mglSampleTimestampsCommand.swift */, + ); + path = commands; + sourceTree = ""; + }; D0F6B96123B6D9E700B45409 = { isa = PBXGroup; children = ( @@ -123,6 +237,7 @@ D0F6B96C23B6D9E700B45409 /* mglMetal */ = { isa = PBXGroup; children = ( + 4DCD58452B36159300E08547 /* commands */, D0F6B97723B6D9E800B45409 /* mglMetal.entitlements */, 4DA4671B2763C16200B65B9F /* mglCommandTypes.h */, D0F6B99D23B6DB3A00B45409 /* mglMetal-Bridging-Header.h */, @@ -135,7 +250,7 @@ 4D5619102762A8F4009AB2E2 /* mglLocalClient.swift */, 4D56190A27628883009AB2E2 /* mglLocalServer.swift */, D0F6B9A123B6ED4300B45409 /* mglPrimitives.swift */, - D0F6B99923B6DAD600B45409 /* mglRenderer.swift */, + 4DCD584A2B36335500E08547 /* mglRenderer2.swift */, 4DA4671E2763CB8E00B65B9F /* mglServer.swift */, D0F6B96F23B6D9E700B45409 /* mglViewController.swift */, D0F6B97123B6D9E800B45409 /* Assets.xcassets */, @@ -143,6 +258,7 @@ 4D53454C28205AA400B61D3D /* mglColorRenderingConfig.swift */, 4D8B32802824627800278B6F /* mglDepthStencilConfig.swift */, 4D5882B1285A89FD005DDF00 /* mglCommandModel.swift */, + 4D5782CA2B472AAE0050EF81 /* mglLogger.swift */, ); path = mglMetal; sourceTree = ""; @@ -298,18 +414,54 @@ buildActionMask = 2147483647; files = ( 4DA4671F2763CB8E00B65B9F /* mglServer.swift in Sources */, + 4D5F72B32B45DD9B00B4DA29 /* mglSetViewColorPixelFormatCommand.swift in Sources */, + 4D5782CB2B472AAE0050EF81 /* mglLogger.swift in Sources */, 4D8B32812824627800278B6F /* mglDepthStencilConfig.swift in Sources */, 4D53454D28205AA400B61D3D /* mglColorRenderingConfig.swift in Sources */, + 4D5F72AD2B45D8BD00B4DA29 /* mglWindowedCommand.swift in Sources */, + 4D5782D32B474E820050EF81 /* mglBltTextureCommand.swift in Sources */, + 4D5782DF2B4761350050EF81 /* mglRepeatFlickerCommand.swift in Sources */, + 4D5F72AF2B45D93500B4DA29 /* mglSetWindowFrameInDisplayCommand.swift in Sources */, + 4DCD58492B36178E00E08547 /* mglSetClearColorCommand.swift in Sources */, 4D56190B27628883009AB2E2 /* mglLocalServer.swift in Sources */, 4D5619112762A8F4009AB2E2 /* mglLocalClient.swift in Sources */, + 4D5F72BD2B45E62900B4DA29 /* mglSelectStencilCommand.swift in Sources */, + 4D5F729F2B45B6C000B4DA29 /* mglSetRenderTargetCommand.swift in Sources */, + 4D5782DB2B4755010050EF81 /* mglArcsConmmand.swift in Sources */, + 4D5F72B12B45DAE800B4DA29 /* mglGetWindowFrameInDisplayCommand.swift in Sources */, + 4D5F72A92B45D60B00B4DA29 /* mglDrainSystemEventsCommand.swift in Sources */, + 4D5782E72B476A370050EF81 /* mglRepeatFlushCommand.swift in Sources */, + 4D5782D12B474DA30050EF81 /* mglMinimizeCommand.swift in Sources */, + 4D5F72BB2B45E5DF00B4DA29 /* mglFinishStencilCreationCommand.swift in Sources */, + 4D5F72B72B45E24F00B4DA29 /* mglSetXformCommand.swift in Sources */, + 4DCD584B2B36335500E08547 /* mglRenderer2.swift in Sources */, + 4D5782E32B4767730050EF81 /* mglRepeatQuadsCommand.swift in Sources */, D08D4BBE23B8834C00A27C79 /* mglCommandInterface.swift in Sources */, + 4D5F72A32B45CEFA00B4DA29 /* mglReadTextureCommand.swift in Sources */, + 4D5F72A72B45D53500B4DA29 /* mglPingCommand.swift in Sources */, + 4D5782D72B4753AC0050EF81 /* mglLineCommand.swift in Sources */, D0F6B97023B6D9E700B45409 /* mglViewController.swift in Sources */, - D0F6B99A23B6DAD600B45409 /* mglRenderer.swift in Sources */, + 4D5F72B92B45E4D500B4DA29 /* mglStartStencilCreationCommand.swift in Sources */, + 4D5782CF2B474BA60050EF81 /* mglFrameGrabCommand.swift in Sources */, 4D5882B2285A89FD005DDF00 /* mglCommandModel.swift in Sources */, + 4D5F72BF2B45E87600B4DA29 /* mglQuadCommand.swift in Sources */, D0F6B9A223B6ED4300B45409 /* mglPrimitives.swift in Sources */, D0A59C7A23C2E41900D3AE93 /* mglSecs.m in Sources */, + 4D5F72A52B45D25900B4DA29 /* mglDeleteTextureCommand.swift in Sources */, D0F6B96E23B6D9E700B45409 /* AppDelegate.swift in Sources */, + 4D5782D92B47547C0050EF81 /* mglPolygonCommand.swift in Sources */, + 4D5782D52B4752D00050EF81 /* mglDotsCommand.swift in Sources */, + 4D5782C92B4722110050EF81 /* mglInfoCommand.swift in Sources */, D0F6B9A423B6EDE600B45409 /* mglShaders.metal in Sources */, + 4D5F72AB2B45D75300B4DA29 /* mglFullscreenCommand.swift in Sources */, + 4D5782E12B4764F60050EF81 /* mglRepeatBltsCommand.swift in Sources */, + 4D5782DD2B4757520050EF81 /* mglUpdateTextureCommand.swift in Sources */, + 4D5782CD2B4748530050EF81 /* mglGetErrorMessageCommand.swift in Sources */, + 4D5F729D2B45B07600B4DA29 /* mglCreateTextureCommand.swift in Sources */, + 4DCD58472B3615BF00E08547 /* mglFlushCommand.swift in Sources */, + 4D755E062B6941FB00ACB083 /* mglSampleTimestampsCommand.swift in Sources */, + 4D5F72B52B45E0A500B4DA29 /* mglDisplayCursorCommand.swift in Sources */, + 4D5782E52B47693D0050EF81 /* mglRepeatDotsCommand.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -483,6 +635,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = gru.mglMetal; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -505,6 +658,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = gru.mglMetal; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "mglMetal/mglMetal-Bridging-Header.h"; @@ -566,6 +720,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = gru.mglMetalUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -585,6 +740,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = gru.mglMetalUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/metal/mglMetal/commands/mglArcsConmmand.swift b/metal/mglMetal/commands/mglArcsConmmand.swift new file mode 100644 index 00000000..2add5162 --- /dev/null +++ b/metal/mglMetal/commands/mglArcsConmmand.swift @@ -0,0 +1,104 @@ +// +// mglArcsConmmand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglArcsCommand : mglCommand { + private let centerVertex: MTLBuffer + private let arcCount: Int + + init(centerVertex: MTLBuffer, arcCount: Int) { + self.centerVertex = centerVertex + self.arcCount = arcCount + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface, device: MTLDevice) { + // read the center vertex for the arc from the commandInterface + // extra values are rgba (1x4), radii (1x4), wedge (1x2), border (1x1) + guard let (centerVertex, arcCount) = commandInterface.readVertices(device: device, extraVals: 11) else { + return nil + } + self.centerVertex = centerVertex + self.arcCount = arcCount + super.init(framesRemaining: 1) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // get an MTLBuffer from the GPU for storing the vertices for two triangles (i.e. + // we are going to make a square around where the arc is going to be drawn, and + // then color the pixels in the fragment shader according to how far they are away + // from the center.) Note that the vertices will have 3 + 2 more values than the + // centerVertex passed in, because each of these vertices will get the xyz of the + // centerVertex added on (which is used for the calculation for how far away each + // pixel is from the center in the fragment shader) and the viewport dimensions + let byteCount = 6 * ((centerVertex.length/arcCount) + 5 * MemoryLayout.stride); + guard let triangleVertices = view.device?.makeBuffer(length: byteCount * arcCount, options: .storageModeManaged) else { + logger.error(component: "mglArcsCommand", details: "Could not make vertex buffer of size \(byteCount)") + return false + } + + // get size of buffer as number of floats, note that we add + // 3 floats for the center position plus 2 floats for the viewport dimensions + let vertexBufferSize = 5 + (centerVertex.length/arcCount)/MemoryLayout.stride; + + // get pointers to the buffer that we will pass to the renderer + let triangleVerticesPointer = triangleVertices.contents().assumingMemoryBound(to: Float.self); + + // get the viewport size, which may be the on-screen view or an offscreen texture + let (viewportWidth, viewportHeight) = colorRenderingState.getSize(view: view) + + // iterate over how many vertices (i.e. how many arcs) that the user passed in + for iArc in 0...stride + // Now create the vertices of each corner of the triangles by copying + // the centerVertex in and then modifying the x, y location appropriately + // get desired x and y locations of the triangle corners + let x = centerVertexPointer[0]; + let y = centerVertexPointer[1]; + // radius is the outer radius + half the border + let rX = centerVertexPointer[8]+centerVertexPointer[13]/2; + let rY = centerVertexPointer[10]+centerVertexPointer[13]/2; + let xLocs: [Float] = [x-rX, x-rX, x+rX, x-rX, x+rX, x+rX] + let yLocs: [Float] = [y-rY, y+rY, y+rY, y-rY, y-rY, y+rY] + + // iterate over 6 vertices (which will be the corners of the triangles) + for iVertex in 0...5 { + // get a pointer to the location in the triangleVertices where we want to copy into + let thisTriangleVerticesPointer = triangleVerticesPointer + iVertex*vertexBufferSize + iArc*vertexBufferSize*6; + // and copy the center vertex into each location + memcpy(thisTriangleVerticesPointer, centerVertexPointer, centerVertex.length/arcCount); + // now set the xy location + thisTriangleVerticesPointer[0] = xLocs[iVertex]; + thisTriangleVerticesPointer[1] = yLocs[iVertex]; + // and set the centerVertex + thisTriangleVerticesPointer[14] = centerVertexPointer[0] + thisTriangleVerticesPointer[15] = -centerVertexPointer[1] + thisTriangleVerticesPointer[16] = centerVertexPointer[2] + // and set viewport dimension + thisTriangleVerticesPointer[17] = viewportWidth + thisTriangleVerticesPointer[18] = viewportHeight + } + + } + + // Draw all the arcs + renderEncoder.setRenderPipelineState(colorRenderingState.getArcsPipelineState()) + renderEncoder.setVertexBuffer(triangleVertices, offset: 0, index: 0) + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6*arcCount) + return true + } +} diff --git a/metal/mglMetal/commands/mglBltTextureCommand.swift b/metal/mglMetal/commands/mglBltTextureCommand.swift new file mode 100644 index 00000000..f12337fe --- /dev/null +++ b/metal/mglMetal/commands/mglBltTextureCommand.swift @@ -0,0 +1,115 @@ +// +// mglBltTextureCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglBltTextureCommand : mglCommand { + private let minMagFilter: MTLSamplerMinMagFilter + private let mipFilter: MTLSamplerMipFilter + private let addressMode: MTLSamplerAddressMode + private let vertexBufferTexture: MTLBuffer + private let vertexCount: Int + private var phase: Float32 + private let textureNumber: UInt32 + + init( + minMagFilter: MTLSamplerMinMagFilter = .linear, + mipFilter: MTLSamplerMipFilter = .linear, + addressMode: MTLSamplerAddressMode = .repeat, + vertexBufferTexture: MTLBuffer, + vertexCount: Int, + phase: Float32 = 0.0, + textureNumber: UInt32 + ) { + self.minMagFilter = minMagFilter + self.mipFilter = mipFilter + self.addressMode = addressMode + self.vertexBufferTexture = vertexBufferTexture + self.vertexCount = vertexCount + self.phase = phase + self.textureNumber = textureNumber + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface, device: MTLDevice) { + guard let minMagFilterRawValue = commandInterface.readUInt32(), + let mipFilterRawValue = commandInterface.readUInt32(), + let addressModeRawValue = commandInterface.readUInt32(), + let (vertexBufferTexture, vertexCount) = commandInterface.readVertices(device: device, extraVals: 2), + let phase = commandInterface.readFloat(), + let textureNumber = commandInterface.readUInt32() else { + return nil + } + self.minMagFilter = chooseMinMagFilter(rawValue: minMagFilterRawValue) + self.mipFilter = chooseMipFilter(rawValue: mipFilterRawValue) + self.addressMode = chooseAddressMode(rawValue: addressModeRawValue) + self.vertexBufferTexture = vertexBufferTexture + self.vertexCount = vertexCount + self.phase = phase + self.textureNumber = textureNumber + super.init(framesRemaining: 1) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // Make sure we have the actual requested texture. + guard let texture = colorRenderingState.getTexture(textureNumber: textureNumber), + let device = view.device else { + return false + } + + // Set up texture sampling and filtering. + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = minMagFilter + samplerDescriptor.magFilter = minMagFilter + samplerDescriptor.mipFilter = mipFilter + samplerDescriptor.sAddressMode = addressMode + samplerDescriptor.tAddressMode = addressMode + samplerDescriptor.rAddressMode = addressMode + let samplerState = device.makeSamplerState(descriptor: samplerDescriptor) + + // Draw vertices as points with 5 values per vertex: [xyz uv]. + renderEncoder.setRenderPipelineState(colorRenderingState.getTexturePipelineState()) + renderEncoder.setVertexBuffer(vertexBufferTexture, offset: 0, index: 0) + renderEncoder.setFragmentSamplerState(samplerState, index: 0) + renderEncoder.setFragmentBytes(&phase, length: MemoryLayout.stride, index: 2) + renderEncoder.setFragmentTexture(texture, index:0) + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount) + + return true + } + +} + +private func chooseMinMagFilter(rawValue: UInt32, defaultValue: MTLSamplerMinMagFilter = .linear) -> MTLSamplerMinMagFilter { + guard let filter = MTLSamplerMinMagFilter(rawValue: UInt(rawValue)) else { + return defaultValue + } + return filter +} + +private func chooseMipFilter(rawValue: UInt32, defaultValue: MTLSamplerMipFilter = .linear) -> MTLSamplerMipFilter { + guard let filter = MTLSamplerMipFilter(rawValue: UInt(rawValue)) else { + return defaultValue + } + return filter +} + +private func chooseAddressMode(rawValue: UInt32, defaultValue: MTLSamplerAddressMode = .repeat) -> MTLSamplerAddressMode { + guard let filter = MTLSamplerAddressMode(rawValue: UInt(rawValue)) else { + return defaultValue + } + return filter +} diff --git a/metal/mglMetal/commands/mglCreateTextureCommand.swift b/metal/mglMetal/commands/mglCreateTextureCommand.swift new file mode 100644 index 00000000..de09e925 --- /dev/null +++ b/metal/mglMetal/commands/mglCreateTextureCommand.swift @@ -0,0 +1,59 @@ +// +// mglCreateTextureCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglCreateTextureCommand : mglCommand { + private let texture: MTLTexture + var textureNumber: UInt32 = 0 + var textureCount: UInt32 = 0 + + init(texture: MTLTexture) { + self.texture = texture + super.init() + } + + init?(commandInterface: mglCommandInterface, device: MTLDevice) { + guard let incomingTexture = commandInterface.createTexture(device: device) else { + return nil + } + texture = incomingTexture + super.init() + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + textureNumber = colorRenderingState.addTexture(texture: texture) + textureCount = colorRenderingState.getTextureCount() + return true + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + if (textureNumber < 1) { + // A heads up that something went wrong. + _ = commandInterface.writeDouble(data: -commandInterface.secs.get()) + } + + // A heads up that return data is on the way. + _ = commandInterface.writeDouble(data: commandInterface.secs.get()) + + // Specific return data for this command. + _ = commandInterface.writeUInt32(data: textureNumber) + _ = commandInterface.writeUInt32(data: textureCount) + return true + } +} diff --git a/metal/mglMetal/commands/mglDeleteTextureCommand.swift b/metal/mglMetal/commands/mglDeleteTextureCommand.swift new file mode 100644 index 00000000..a4a22e31 --- /dev/null +++ b/metal/mglMetal/commands/mglDeleteTextureCommand.swift @@ -0,0 +1,37 @@ +// +// mglDeleteTextureCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglDeleteTextureCommand : mglCommand { + let textureNumber: UInt32 + + init(textureNumber: UInt32) { + self.textureNumber = textureNumber + super.init() + } + + init?(commandInterface: mglCommandInterface) { + guard let textureNumber = commandInterface.readUInt32() else { + return nil + } + self.textureNumber = textureNumber + super.init() + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + return colorRenderingState.removeTexture(textureNumber: textureNumber) != nil + } +} diff --git a/metal/mglMetal/commands/mglDisplayCursorCommand.swift b/metal/mglMetal/commands/mglDisplayCursorCommand.swift new file mode 100644 index 00000000..6007b19d --- /dev/null +++ b/metal/mglMetal/commands/mglDisplayCursorCommand.swift @@ -0,0 +1,56 @@ +// +// mglDisplayCursorCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglDisplayCursorCommand : mglCommand { + // Flag to keep track of cursor state -- for the whole app. + static var cursorHidden = false + + // Whether this is a hide (0) or show (1). + private let displayOrHide: UInt32 + + init(displayOrHide: UInt32) { + self.displayOrHide = displayOrHide + super.init() + } + + init?(commandInterface: mglCommandInterface) { + guard let displayOrHide = commandInterface.readUInt32() else { + return nil + } + self.displayOrHide = displayOrHide + super.init() + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + if displayOrHide == 0 { + // Hide the cursor + if !mglDisplayCursorCommand.cursorHidden { + NSCursor.hide() + mglDisplayCursorCommand.cursorHidden = true + } + } + else { + // Show the cursor + if mglDisplayCursorCommand.cursorHidden { + NSCursor.unhide() + mglDisplayCursorCommand.cursorHidden = false + } + + } + return true + } +} diff --git a/metal/mglMetal/commands/mglDotsCommand.swift b/metal/mglMetal/commands/mglDotsCommand.swift new file mode 100644 index 00000000..208ebe25 --- /dev/null +++ b/metal/mglMetal/commands/mglDotsCommand.swift @@ -0,0 +1,45 @@ +// +// mglDotsCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglDotsCommand : mglCommand { + private let vertexBufferDots: MTLBuffer + private let vertexCount: Int + + init(vertexBufferDots: MTLBuffer, vertexCount: Int) { + self.vertexBufferDots = vertexBufferDots + self.vertexCount = vertexCount + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface, device: MTLDevice) { + guard let (vertexBufferDots, vertexCount) = commandInterface.readVertices(device: device, extraVals: 8) else { + return nil + } + self.vertexBufferDots = vertexBufferDots + self.vertexCount = vertexCount + super.init(framesRemaining: 1) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // Draw all the vertices as points with 11 values per vertex: [xyz rgba wh isRound borderSize]. + renderEncoder.setRenderPipelineState(colorRenderingState.getDotsPipelineState()) + renderEncoder.setVertexBuffer(vertexBufferDots, offset: 0, index: 0) + renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: vertexCount) + return true + } +} diff --git a/metal/mglMetal/commands/mglDrainSystemEventsCommand.swift b/metal/mglMetal/commands/mglDrainSystemEventsCommand.swift new file mode 100644 index 00000000..36202693 --- /dev/null +++ b/metal/mglMetal/commands/mglDrainSystemEventsCommand.swift @@ -0,0 +1,32 @@ +// +// mglDrainSystemEventsCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglDrainSystemEventsCommand : mglCommand { + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + guard let window = view.window else { + logger.error(component: "mglDrainSystemEventsCommand", details: "Could not get window from view, skipping drain events command.") + return false + } + + var event = window.nextEvent(matching: .any) + while (event != nil) { + logger.info(component: "mglDrainSystemEventsCommand", details: "Processing OS event: \(String(describing: event))") + event = window.nextEvent(matching: .any) + } + return true + } +} diff --git a/metal/mglMetal/commands/mglFinishStencilCreationCommand.swift b/metal/mglMetal/commands/mglFinishStencilCreationCommand.swift new file mode 100644 index 00000000..844a99f6 --- /dev/null +++ b/metal/mglMetal/commands/mglFinishStencilCreationCommand.swift @@ -0,0 +1,22 @@ +// +// mglStartStencilCreationCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglFinishStencilCreationCommand : mglCommand { + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + return depthStencilState.finishStencilCreation(view: view) + } +} diff --git a/metal/mglMetal/commands/mglFlushCommand.swift b/metal/mglMetal/commands/mglFlushCommand.swift new file mode 100644 index 00000000..2c915f55 --- /dev/null +++ b/metal/mglMetal/commands/mglFlushCommand.swift @@ -0,0 +1,21 @@ +// +// mglFlushCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 12/22/23. +// Copyright © 2023 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +// Flush is the simplest command, acting as a placeholder to tell us when a frame should be presented. +class mglFlushCommand : mglCommand { + init() { + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface) { + super.init(framesRemaining: 1) + } +} diff --git a/metal/mglMetal/commands/mglFrameGrabCommand.swift b/metal/mglMetal/commands/mglFrameGrabCommand.swift new file mode 100644 index 00000000..d8d5427b --- /dev/null +++ b/metal/mglMetal/commands/mglFrameGrabCommand.swift @@ -0,0 +1,55 @@ +// +// mglFrameGrabCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglFrameGrabCommand : mglCommand { + private var width: Int = 0 + private var height: Int = 0 + private var dataPointer: UnsafeMutablePointer? = nil + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + // grab from from the currentColorRenderingTarget. Note that + // this will return (0,0,nil) if the current target is the screen + // as it is not implemented (and might be hard/impossible?) to get + // the bytes from that. So, this only works if the current target + // is a texture (which is set by mglMetalSetRenderTarget + (width, height, dataPointer) = colorRenderingState.frameGrab() + return dataPointer != nil + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + // write out the width and height + _ = commandInterface.writeUInt32(data: UInt32(width)) + _ = commandInterface.writeUInt32(data: UInt32(height)) + if dataPointer != nil { + // convert the pointer back into an array + let floatArray = Array(UnsafeBufferPointer(start: dataPointer, count: width * height * 4)) + + // write the array + _ = commandInterface.writeFloatArray(data: floatArray) + + // free the data + dataPointer?.deallocate() + return true + } + else { + return false + } + } +} diff --git a/metal/mglMetal/commands/mglFullscreenCommand.swift b/metal/mglMetal/commands/mglFullscreenCommand.swift new file mode 100644 index 00000000..66f81729 --- /dev/null +++ b/metal/mglMetal/commands/mglFullscreenCommand.swift @@ -0,0 +1,35 @@ +// +// mglFullscreenCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit +import OSLog + +class mglFullscreenCommand : mglCommand { + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + guard let window = view.window else { + logger.error(component: "mglFullscreenCommand", details: "Could not get window from view, skipping fullscreen command.") + return false + } + + if window.styleMask.contains(.fullScreen) { + logger.info(component: "mglFullscreenCommand", details: " App is already fullscreen, skipping fullscreen command.") + } else { + window.toggleFullScreen(nil) + NSCursor.hide() + mglDisplayCursorCommand.cursorHidden = true + } + return true + } +} diff --git a/metal/mglMetal/commands/mglGetErrorMessageCommand.swift b/metal/mglMetal/commands/mglGetErrorMessageCommand.swift new file mode 100644 index 00000000..25088950 --- /dev/null +++ b/metal/mglMetal/commands/mglGetErrorMessageCommand.swift @@ -0,0 +1,20 @@ +// +// mglGetErrorMessageCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation + +class mglGetErrorMessageCommand : mglCommand { + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + // Our mgl logger remembers the last error message! + _ = commandInterface.writeString(data: logger.getErrorMessage()) + return true + } +} diff --git a/metal/mglMetal/commands/mglGetWindowFrameInDisplayCommand.swift b/metal/mglMetal/commands/mglGetWindowFrameInDisplayCommand.swift new file mode 100644 index 00000000..00b59735 --- /dev/null +++ b/metal/mglMetal/commands/mglGetWindowFrameInDisplayCommand.swift @@ -0,0 +1,74 @@ +// +// mglFullscreenCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglGetWindowFrameInDisplayCommand : mglCommand { + private var displayNumber: UInt32 = 0 + private var windowX: UInt32 = 0 + private var windowY: UInt32 = 0 + private var windowWidth: UInt32 = 0 + private var windowHeight: UInt32 = 0 + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + guard let window = view.window else { + logger.error(component: "mglGetWindowFrameInDisplayCommand", details: "Could get window from view, skipping get window frame command.") + return false + } + + guard let screen = window.screen else { + logger.error(component: "mglGetWindowFrameInDisplayCommand", details: "Could get screen from window, skipping get window frame command.") + return false + } + + guard let screenIndex = NSScreen.screens.firstIndex(of: screen) else { + logger.error(component: "mglGetWindowFrameInDisplayCommand", details: "Could get screen index from screens, skipping get window frame command.") + return false + } + + // Convert 0-based screen index to Matlab's 1-based display number. + displayNumber = UInt32(screenIndex + 1) + + // Return the position of the window relative to its screen, in pixel units not hi-res "points". + let windowNativeFrame = screen.convertRectToBacking(window.frame) + let screenNativeFrame = screen.convertRectToBacking(screen.frame) + windowX = UInt32(windowNativeFrame.origin.x - screenNativeFrame.origin.x) + windowY = UInt32(windowNativeFrame.origin.y - screenNativeFrame.origin.y) + windowWidth = UInt32(windowNativeFrame.width) + windowHeight = UInt32(windowNativeFrame.height) + return true + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + if displayNumber < 1 { + _ = commandInterface.writeDouble(data: -commandInterface.secs.get()) + return false + } + + // A heads up that return data is on the way. + _ = commandInterface.writeDouble(data: commandInterface.secs.get()) + + // Specific return data for this command. + _ = commandInterface.writeUInt32(data: displayNumber) + _ = commandInterface.writeUInt32(data: windowX) + _ = commandInterface.writeUInt32(data: windowY) + _ = commandInterface.writeUInt32(data: windowWidth) + _ = commandInterface.writeUInt32(data: windowHeight) + return true + } +} diff --git a/metal/mglMetal/commands/mglInfoCommand.swift b/metal/mglMetal/commands/mglInfoCommand.swift new file mode 100644 index 00000000..5b4d08d4 --- /dev/null +++ b/metal/mglMetal/commands/mglInfoCommand.swift @@ -0,0 +1,116 @@ +// +// mglInfoCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglInfoCommand : mglCommand { + var device: MTLDevice! + var view: MTKView! + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + // Stash references to the current app view and device for readout, below. + // This is a little unusual compared to other commands. + guard let device = view.device else { + return false + } + self.device = device + self.view = view + return true + } + + override func writeQueryResults(logger: mglLogger, commandInterface : mglCommandInterface) -> Bool { + // send GPU name + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "gpu.name") + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: device.name) + + // send GPU registryID + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "gpu.registryID") + _ = commandInterface.writeCommand(data: mglSendDouble) + _ = commandInterface.writeDouble(data: Double(device.registryID)) + + // send currentAllocatedSize + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "gpu.currentAllocatedSize") + _ = commandInterface.writeCommand(data: mglSendDouble) + _ = commandInterface.writeDouble(data: Double(device.currentAllocatedSize)) + + // send recommendedMaxWorkingSetSize + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "gpu.recommendedMaxWorkingSetSize") + _ = commandInterface.writeCommand(data: mglSendDouble) + _ = commandInterface.writeDouble(data: Double(device.recommendedMaxWorkingSetSize)) + + // send hasUnifiedMemory + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "gpu.hasUnifiedMemory") + _ = commandInterface.writeCommand(data: mglSendDouble) + _ = commandInterface.writeDouble(data: device.hasUnifiedMemory ? 1.0 : 0.0) + + // send maxTransferRate + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "gpu.maxTransferRate") + _ = commandInterface.writeCommand(data: mglSendDouble) + _ = commandInterface.writeDouble(data: Double(device.maxTransferRate)) + + // send minimumLinearTextureAlignment + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "gpu.minimumTextureBufferAlignment") + _ = commandInterface.writeCommand(data: mglSendDouble) + _ = commandInterface.writeDouble(data: Double(device.minimumTextureBufferAlignment(for: .rgba32Float))) + + // send minimumLinearTextureAlignment + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "gpu.minimumLinearTextureAlignment") + _ = commandInterface.writeCommand(data: mglSendDouble) + _ = commandInterface.writeDouble(data: Double(device.minimumLinearTextureAlignment(for: .rgba32Float))) + + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "view.colorPixelFormat") + _ = commandInterface.writeCommand(data: mglSendDouble) + _ = commandInterface.writeDouble(data: Double(view.colorPixelFormat.rawValue)) + + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "view.colorPixelFormatString") + _ = commandInterface.writeCommand(data: mglSendString) + switch view.colorPixelFormat { + case MTLPixelFormat.bgra8Unorm: _ = commandInterface.writeString(data: "bgra8Unorm") + case MTLPixelFormat.bgra8Unorm_srgb: _ = commandInterface.writeString(data: "bgra8Unorm_srgb") + case MTLPixelFormat.rgba16Float: _ = commandInterface.writeString(data: "rgba16Float") + case MTLPixelFormat.rgb10a2Unorm: _ = commandInterface.writeString(data: "rgb10a2Unorm") + case MTLPixelFormat.bgr10a2Unorm: _ = commandInterface.writeString(data: "bgr10a2Unorm") + default: _ = commandInterface.writeString(data: "Unknown") + } + + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "view.clearColor") + _ = commandInterface.writeCommand(data: mglSendDoubleArray) + let colorArray: [Double] = [Double(view.clearColor.red),Double(view.clearColor.green),Double(view.clearColor.blue),Double(view.clearColor.alpha)] + _ = commandInterface.writeDoubleArray(data: colorArray) + + _ = commandInterface.writeCommand(data: mglSendString) + _ = commandInterface.writeString(data: "view.drawableSize") + _ = commandInterface.writeCommand(data: mglSendDoubleArray) + let drawableSize: [Double] = [Double(view.drawableSize.width), Double(view.drawableSize.height)] + _ = commandInterface.writeDoubleArray(data: drawableSize) + + // send finished + _ = commandInterface.writeCommand(data: mglSendFinished) + + return true + } +} diff --git a/metal/mglMetal/commands/mglLineCommand.swift b/metal/mglMetal/commands/mglLineCommand.swift new file mode 100644 index 00000000..3f482697 --- /dev/null +++ b/metal/mglMetal/commands/mglLineCommand.swift @@ -0,0 +1,46 @@ +// +// mglLineCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglLineCommand : mglCommand { + private let vertexBufferWithColors: MTLBuffer + private let vertexCount: Int + + init(vertexBufferWithColors: MTLBuffer, vertexCount: Int) { + self.vertexBufferWithColors = vertexBufferWithColors + self.vertexCount = vertexCount + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface, device: MTLDevice) { + // Read and buffer vertices as points with 6 values per vertex: [xyz rgb] + guard let (vertexBufferWithColors, vertexCount) = commandInterface.readVertices(device: device, extraVals: 3) else { + return nil + } + self.vertexBufferWithColors = vertexBufferWithColors + self.vertexCount = vertexCount + super.init(framesRemaining: 1) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // Render vertices as unconnected lines, expect separate paris of vertices per line. + renderEncoder.setRenderPipelineState(colorRenderingState.getVerticesWithColorPipelineState()) + renderEncoder.setVertexBuffer(vertexBufferWithColors, offset: 0, index: 0) + renderEncoder.drawPrimitives(type: .line, vertexStart: 0, vertexCount: vertexCount) + return true + } +} diff --git a/metal/mglMetal/commands/mglMinimizeCommand.swift b/metal/mglMetal/commands/mglMinimizeCommand.swift new file mode 100644 index 00000000..785c9eb7 --- /dev/null +++ b/metal/mglMetal/commands/mglMinimizeCommand.swift @@ -0,0 +1,45 @@ +// +// mglMinimizeCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglMinimizeCommand : mglCommand { + private let minimizeOrRestore: UInt32 + + init(minimizeOrRestore: UInt32) { + self.minimizeOrRestore = minimizeOrRestore + super.init() + } + + init?(commandInterface: mglCommandInterface) { + // Get whether this is a minimize (0) or restore (1) + guard let minimizeOrRestore = commandInterface.readUInt32() else { + return nil + } + self.minimizeOrRestore = minimizeOrRestore + super.init() + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + if minimizeOrRestore == 0 { + // minimize + view.window?.miniaturize(nil) + } else { + // restore + view.window?.deminiaturize(nil) + } + return true + } +} diff --git a/metal/mglMetal/commands/mglPingCommand.swift b/metal/mglMetal/commands/mglPingCommand.swift new file mode 100644 index 00000000..5d45edde --- /dev/null +++ b/metal/mglMetal/commands/mglPingCommand.swift @@ -0,0 +1,18 @@ +// +// mglPingCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation + +class mglPingCommand : mglCommand { + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + return commandInterface.writeCommand(data: mglPing) == mglSizeOfCommandCodeArray(1) + } +} diff --git a/metal/mglMetal/commands/mglPolygonCommand.swift b/metal/mglMetal/commands/mglPolygonCommand.swift new file mode 100644 index 00000000..680a5892 --- /dev/null +++ b/metal/mglMetal/commands/mglPolygonCommand.swift @@ -0,0 +1,46 @@ +// +// mglPolygonCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglPolygonCommand : mglCommand { + private let vertexBufferWithColors: MTLBuffer + private let vertexCount: Int + + init(vertexBufferWithColors: MTLBuffer, vertexCount: Int) { + self.vertexBufferWithColors = vertexBufferWithColors + self.vertexCount = vertexCount + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface, device: MTLDevice) { + // Read and buffer vertices as points with 6 values per vertex: [xyz rgb] + guard let (vertexBufferWithColors, vertexCount) = commandInterface.readVertices(device: device, extraVals: 3) else { + return nil + } + self.vertexBufferWithColors = vertexBufferWithColors + self.vertexCount = vertexCount + super.init(framesRemaining: 1) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // Render vertices as a connected triangle strip, expecting vertices to alternate sides of a convex polygon. + renderEncoder.setRenderPipelineState(colorRenderingState.getVerticesWithColorPipelineState()) + renderEncoder.setVertexBuffer(vertexBufferWithColors, offset: 0, index: 0) + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertexCount) + return true + } +} diff --git a/metal/mglMetal/commands/mglQuadCommand.swift b/metal/mglMetal/commands/mglQuadCommand.swift new file mode 100644 index 00000000..889b50fc --- /dev/null +++ b/metal/mglMetal/commands/mglQuadCommand.swift @@ -0,0 +1,46 @@ +// +// mglQuadCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglQuadCommand : mglCommand { + private let vertexBufferWithColors: MTLBuffer + private let vertexCount: Int + + init(vertexBufferWithColors: MTLBuffer, vertexCount: Int) { + self.vertexBufferWithColors = vertexBufferWithColors + self.vertexCount = vertexCount + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface, device: MTLDevice) { + // Read and buffer vertices as points with 6 values per vertex: [xyz rgb] + guard let (vertexBufferWithColors, vertexCount) = commandInterface.readVertices(device: device, extraVals: 3) else { + return nil + } + self.vertexBufferWithColors = vertexBufferWithColors + self.vertexCount = vertexCount + super.init(framesRemaining: 1) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // Render vertices as triangles, expect two triangles per quad. + renderEncoder.setRenderPipelineState(colorRenderingState.getVerticesWithColorPipelineState()) + renderEncoder.setVertexBuffer(vertexBufferWithColors, offset: 0, index: 0) + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount) + return true + } +} diff --git a/metal/mglMetal/commands/mglReadTextureCommand.swift b/metal/mglMetal/commands/mglReadTextureCommand.swift new file mode 100644 index 00000000..99d48af4 --- /dev/null +++ b/metal/mglMetal/commands/mglReadTextureCommand.swift @@ -0,0 +1,74 @@ +// +// mglReadTextureCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglReadTextureCommand : mglCommand { + let textureNumber: UInt32 + private var texture: MTLTexture? + + init(textureNumber: UInt32) { + self.textureNumber = textureNumber + super.init() + } + + init?(commandInterface: mglCommandInterface) { + guard let textureNumber = commandInterface.readUInt32() else { + return nil + } + self.textureNumber = textureNumber + super.init() + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + guard let existingTexture = colorRenderingState.getTexture(textureNumber: textureNumber) else { + return false + } + texture = existingTexture + return true + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + guard let unboxedTexture = texture else { + logger.error(component: "mglReadTextureCommand", details: "Unable to read null texture") + _ = commandInterface.writeDouble(data: -commandInterface.secs.get()) + return false + } + + guard let buffer = unboxedTexture.buffer else { + logger.error(component: "mglReadTextureCommand", details: "Unable to access buffer of texture \(String(describing: unboxedTexture))") + _ = commandInterface.writeDouble(data: -commandInterface.secs.get()) + return false + } + + // A heads up that return data is on the way. + _ = commandInterface.writeDouble(data: commandInterface.secs.get()) + + // Specific return data for this command. + let imageRowByteCount = Int(mglSizeOfFloatRgbaTexture(mglUInt32(unboxedTexture.width), 1)) + _ = commandInterface.writeUInt32(data: mglUInt32(unboxedTexture.width)) + _ = commandInterface.writeUInt32(data: mglUInt32(unboxedTexture.height)) + let totalByteCount = commandInterface.imageRowsFromBuffer( + buffer: buffer, + imageRowByteCount: imageRowByteCount, + alignedRowByteCount: unboxedTexture.bufferBytesPerRow, + rowCount: unboxedTexture.height + ) + return totalByteCount == imageRowByteCount * unboxedTexture.height + } +} diff --git a/metal/mglMetal/commands/mglRepeatBltsCommand.swift b/metal/mglMetal/commands/mglRepeatBltsCommand.swift new file mode 100644 index 00000000..214967e0 --- /dev/null +++ b/metal/mglMetal/commands/mglRepeatBltsCommand.swift @@ -0,0 +1,102 @@ +// +// mglRepeatBltsCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglRepeatBltsCommand : mglCommand { + private let repeatCount: UInt32 + + private var secs = mglSecs() + private var drawTime: Double = 0.0 + + init(repeatCount: UInt32) { + self.repeatCount = repeatCount + super.init(framesRemaining: Int(repeatCount)) + } + + init?(commandInterface: mglCommandInterface) { + guard let repeatCount = commandInterface.readUInt32() else { + return nil + } + self.repeatCount = repeatCount + super.init(framesRemaining: Int(repeatCount)) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + + // For now, choose arbitrary vertices to blt onto. + let vertexByteCount = Int(mglSizeOfFloatVertexArray(6, 5)) + guard let vertexBuffer = view.device?.makeBuffer(length: vertexByteCount, options: .storageModeManaged) else { + logger.error(component: "mglRepeatBltsCommand", details: "Could not make vertex buffer of size \(vertexByteCount)") + return false + } + let vertexData: [Float32] = [ + 1, 1, 0, 1, 0, + -1, 1, 0, 0, 0, + -1, -1, 0, 0, 1, + 1, 1, 0, 1, 0, + -1, -1, 0, 0, 1, + 1, -1, 0, 1, 1 + ] + let bufferFloats = vertexBuffer.contents().bindMemory(to: Float32.self, capacity: vertexData.count) + bufferFloats.update(from: vertexData, count: vertexData.count) + + // Choose a next texture from the available textures, varying with the repeating command count. + let textureNumbers = colorRenderingState.getTextureNumbers() + let textureIndex = framesRemaining % textureNumbers.count + let textureNumber = textureNumbers[textureIndex] + guard let texture = colorRenderingState.getTexture(textureNumber: textureNumber) else { + return false + } + + // For now, choose an arbitrary, fixed sampling strategy. + let samplerDescriptor = MTLSamplerDescriptor() + samplerDescriptor.minFilter = .nearest + samplerDescriptor.magFilter = .nearest + samplerDescriptor.mipFilter = .nearest + samplerDescriptor.sAddressMode = .repeat + samplerDescriptor.tAddressMode = .repeat + samplerDescriptor.rAddressMode = .repeat + guard let samplerState = view.device?.makeSamplerState(descriptor:samplerDescriptor) else { + logger.error(component: "mglRepeatBltsCommand", details: "Could not make makeSamplerState.") + return false + } + + // For now, assume drift-phase 0. + var phase = Float32(0) + + renderEncoder.setRenderPipelineState(colorRenderingState.getTexturePipelineState()) + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + renderEncoder.setFragmentSamplerState(samplerState, index: 0) + renderEncoder.setFragmentBytes(&phase, length: MemoryLayout.stride, index: 2) + renderEncoder.setFragmentTexture(texture, index:0) + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) + + // Record draw time to send back to the client. + drawTime = secs.get() + + return true + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + // Report to the client when drawing commands finished up. + _ = commandInterface.writeDouble(data: drawTime) + return true + } +} diff --git a/metal/mglMetal/commands/mglRepeatDotsCommand.swift b/metal/mglMetal/commands/mglRepeatDotsCommand.swift new file mode 100644 index 00000000..953a2498 --- /dev/null +++ b/metal/mglMetal/commands/mglRepeatDotsCommand.swift @@ -0,0 +1,107 @@ +// +// mglRepeatDotsCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit +import GameplayKit + +class mglRepeatDotsCommand : mglCommand { + private let repeatCount: UInt32 + private let objectCount: UInt32 + private let randomSeed: UInt32 + private let randomSource: GKMersenneTwisterRandomSource + + private var secs = mglSecs() + private var drawTime: Double = 0.0 + + init(repeatCount: UInt32, objectCount: UInt32, randomSeed: UInt32) { + self.repeatCount = repeatCount + self.objectCount = objectCount + self.randomSeed = randomSeed + self.randomSource = GKMersenneTwisterRandomSource(seed: UInt64(randomSeed)) + super.init(framesRemaining: Int(repeatCount)) + } + + init?(commandInterface: mglCommandInterface) { + guard let repeatCount = commandInterface.readUInt32(), + let objectCount = commandInterface.readUInt32(), + let randomSeed = commandInterface.readUInt32() else { + return nil + } + self.repeatCount = repeatCount + self.objectCount = objectCount + self.randomSeed = randomSeed + self.randomSource = GKMersenneTwisterRandomSource(seed: UInt64(randomSeed)) + super.init(framesRemaining: Int(repeatCount)) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // Pack a vertex buffer with dots: each has 1 vertex and 11 values per vertex vertex: [xyz rgba wh isRound borderSize]. + let vertexCount = Int(objectCount) + let byteCount = Int(mglSizeOfFloatVertexArray(mglUInt32(vertexCount), 11)) + guard let vertexBuffer = view.device?.makeBuffer(length: byteCount, options: .storageModeManaged) else { + logger.error(component: "mglRepeatDotsCommand", details: "Could not make vertex buffer of size \(byteCount)") + return false + } + let bufferFloats = vertexBuffer.contents().bindMemory(to: Float32.self, capacity: vertexCount) + for dotIndex in (0 ..< vertexCount) { + let offset = Int(11 * dotIndex) + packRandomDot(buffer: bufferFloats, offset: offset) + } + + // Draw all the vertices as points with 11 values per vertex: [xyz rgba wh isRound borderSize]. + renderEncoder.setRenderPipelineState(colorRenderingState.getDotsPipelineState()) + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: vertexCount) + + // Record draw time to send back to the client. + drawTime = secs.get() + + return true + } + + // Create a random dot as the next vertex, with 11 elements per vertex, of the given vertex buffer. + private func packRandomDot(buffer: UnsafeMutablePointer, offset: Int) { + // xyz + buffer[offset + 0] = Float32(randomSource.nextUniform() * 2 - 1) + buffer[offset + 1] = Float32(randomSource.nextUniform() * 2 - 1) + buffer[offset + 2] = 0 + + // rgba + buffer[offset + 3] = Float32(randomSource.nextUniform()) + buffer[offset + 4] = Float32(randomSource.nextUniform()) + buffer[offset + 5] = Float32(randomSource.nextUniform()) + buffer[offset + 6] = 1 + + // wh + buffer[offset + 7] = 1 + buffer[offset + 8] = 1 + + // round + buffer[offset + 9] = 0 + + // border size + buffer[offset + 10] = 0 + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + // Report to the client when drawing commands were finished. + _ = commandInterface.writeDouble(data: drawTime) + return true + } +} diff --git a/metal/mglMetal/commands/mglRepeatFlickerCommand.swift b/metal/mglMetal/commands/mglRepeatFlickerCommand.swift new file mode 100644 index 00000000..17d515dc --- /dev/null +++ b/metal/mglMetal/commands/mglRepeatFlickerCommand.swift @@ -0,0 +1,68 @@ +// +// mglRepeatFlickerCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit +import GameplayKit + +class mglRepeatFlickerCommand : mglCommand { + private let repeatCount: UInt32 + private let randomSeed: UInt32 + private let randomSource: GKMersenneTwisterRandomSource + + private var secs = mglSecs() + private var drawTime: Double = 0.0 + + init(repeatCount: UInt32, randomSeed: UInt32) { + self.repeatCount = repeatCount + self.randomSeed = randomSeed + self.randomSource = GKMersenneTwisterRandomSource(seed: UInt64(randomSeed)) + super.init(framesRemaining: Int(repeatCount)) + } + + init?(commandInterface: mglCommandInterface) { + guard let repeatCount = commandInterface.readUInt32(), + let randomSeed = commandInterface.readUInt32() else { + return nil + } + self.repeatCount = repeatCount + self.randomSeed = randomSeed + self.randomSource = GKMersenneTwisterRandomSource(seed: UInt64(randomSeed)) + super.init(framesRemaining: Int(repeatCount)) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // Choose a new, random color for the view to use on the next render pass. + let r = Double(randomSource.nextUniform()) + let g = Double(randomSource.nextUniform()) + let b = Double(randomSource.nextUniform()) + let clearColor = MTLClearColor(red: r, green: g, blue: b, alpha: 1) + view.clearColor = clearColor + + // Record draw time to send back to the client. + drawTime = secs.get() + + return true + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + // Report to the client when drawing commands were finished. + _ = commandInterface.writeDouble(data: drawTime) + return true + } +} diff --git a/metal/mglMetal/commands/mglRepeatFlushCommand.swift b/metal/mglMetal/commands/mglRepeatFlushCommand.swift new file mode 100644 index 00000000..2f466c33 --- /dev/null +++ b/metal/mglMetal/commands/mglRepeatFlushCommand.swift @@ -0,0 +1,52 @@ +// +// mglRepeatFlushCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglRepeatFlushCommand : mglCommand { + private let repeatCount: UInt32 + + private var secs = mglSecs() + private var drawTime: Double = 0.0 + + init(repeatCount: UInt32, objectCount: UInt32, randomSeed: UInt32) { + self.repeatCount = repeatCount + super.init(framesRemaining: Int(repeatCount)) + } + + init?(commandInterface: mglCommandInterface) { + guard let repeatCount = commandInterface.readUInt32() else { + return nil + } + self.repeatCount = repeatCount + super.init(framesRemaining: Int(repeatCount)) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // Record draw time to send back to the client. + drawTime = secs.get() + return true + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + // Report to the client when drawing commands were finished. + _ = commandInterface.writeDouble(data: drawTime) + return true + } +} diff --git a/metal/mglMetal/commands/mglRepeatQuadsCommand.swift b/metal/mglMetal/commands/mglRepeatQuadsCommand.swift new file mode 100644 index 00000000..017b1f74 --- /dev/null +++ b/metal/mglMetal/commands/mglRepeatQuadsCommand.swift @@ -0,0 +1,149 @@ +// +// mglRepeatQuadsCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit +import GameplayKit + +class mglRepeatQuadsCommand : mglCommand { + private let repeatCount: UInt32 + private let objectCount: UInt32 + private let randomSeed: UInt32 + private let randomSource: GKMersenneTwisterRandomSource + + private var secs = mglSecs() + private var drawTime: Double = 0.0 + + init(repeatCount: UInt32, objectCount: UInt32, randomSeed: UInt32) { + self.repeatCount = repeatCount + self.objectCount = objectCount + self.randomSeed = randomSeed + self.randomSource = GKMersenneTwisterRandomSource(seed: UInt64(randomSeed)) + super.init(framesRemaining: Int(repeatCount)) + } + + init?(commandInterface: mglCommandInterface) { + guard let repeatCount = commandInterface.readUInt32(), + let objectCount = commandInterface.readUInt32(), + let randomSeed = commandInterface.readUInt32() else { + return nil + } + self.repeatCount = repeatCount + self.objectCount = objectCount + self.randomSeed = randomSeed + self.randomSource = GKMersenneTwisterRandomSource(seed: UInt64(randomSeed)) + super.init(framesRemaining: Int(repeatCount)) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + guard let device = view.device else { + return false + } + + // Pack a vertex buffer with quads: each has 6 vertices (two triangels) and 6 values per vertex [xyz rgb]. + let vertexCount = Int(6 * objectCount) + let byteCount = Int(mglSizeOfFloatVertexArray(mglUInt32(vertexCount), 6)) + guard let vertexBuffer = device.makeBuffer(length: byteCount, options: .storageModeManaged) else { + logger.error(component: "mglRepeatQuadsCommand", details: "Could not make vertex buffer of size \(byteCount)") + return false + } + let bufferFloats = vertexBuffer.contents().bindMemory(to: Float32.self, capacity: vertexCount * 6) + for quadIndex in (0 ..< objectCount) { + let offset = Int(6 * 6 * quadIndex) + packRandomQuad(buffer: bufferFloats, offset: offset) + } + + // The buffer here uses storageModeManaged, to match the behavior of mglCommandInterface. + // This means we have to tell the GPU about the modifications we just made using the CPU. + vertexBuffer.didModifyRange(0 ..< byteCount) + + // Render vertices as triangles, two per quad, and 6 values per vertex: [xyz rgb]. + renderEncoder.setRenderPipelineState(colorRenderingState.getVerticesWithColorPipelineState()) + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount) + + // Record draw time to send back to the client. + drawTime = secs.get() + + return true + } + + // Create a random quad as the next 6 triangle vertices ([xyz rgb], so 36 elements total) of the given vertex buffer. + private func packRandomQuad(buffer: UnsafeMutablePointer, offset: Int) { + // Pick four random corners of the quad, vertices 0, 1, 2, 3. + let x0 = Float32(randomSource.nextUniform() * 2 - 1) + let x1 = Float32(randomSource.nextUniform() * 2 - 1) + let x2 = Float32(randomSource.nextUniform() * 2 - 1) + let x3 = Float32(randomSource.nextUniform() * 2 - 1) + let y0 = Float32(randomSource.nextUniform() * 2 - 1) + let y1 = Float32(randomSource.nextUniform() * 2 - 1) + let y2 = Float32(randomSource.nextUniform() * 2 - 1) + let y3 = Float32(randomSource.nextUniform() * 2 - 1) + + // Pick one random color for the whole quad. + let r = Float32(randomSource.nextUniform()) + let g = Float32(randomSource.nextUniform()) + let b = Float32(randomSource.nextUniform()) + + // First triangle of the quad gets vertices, 0, 1, 2. + buffer[offset + 0] = x0 + buffer[offset + 1] = y0 + buffer[offset + 2] = 0 + buffer[offset + 3] = r + buffer[offset + 4] = g + buffer[offset + 5] = b + buffer[offset + 6] = x1 + buffer[offset + 7] = y1 + buffer[offset + 8] = 0 + buffer[offset + 9] = r + buffer[offset + 10] = g + buffer[offset + 11] = b + buffer[offset + 12] = x2 + buffer[offset + 13] = y2 + buffer[offset + 14] = 0 + buffer[offset + 15] = r + buffer[offset + 16] = g + buffer[offset + 17] = b + + // Second triangle of the quad gets vertices, 2, 1, 3. + buffer[offset + 18] = x2 + buffer[offset + 19] = y2 + buffer[offset + 20] = 0 + buffer[offset + 21] = r + buffer[offset + 22] = g + buffer[offset + 23] = b + buffer[offset + 24] = x1 + buffer[offset + 25] = y1 + buffer[offset + 26] = 0 + buffer[offset + 27] = r + buffer[offset + 28] = g + buffer[offset + 29] = b + buffer[offset + 30] = x3 + buffer[offset + 31] = y3 + buffer[offset + 32] = 0 + buffer[offset + 33] = r + buffer[offset + 34] = g + buffer[offset + 35] = b + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + // Report to the client when drawing commands were finished. + _ = commandInterface.writeDouble(data: drawTime) + return true + } +} diff --git a/metal/mglMetal/commands/mglSampleTimestampsCommand.swift b/metal/mglMetal/commands/mglSampleTimestampsCommand.swift new file mode 100644 index 00000000..b78700ca --- /dev/null +++ b/metal/mglMetal/commands/mglSampleTimestampsCommand.swift @@ -0,0 +1,54 @@ +// +// mglSampleTimestamps.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/30/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglSampleTimestampsCommand : mglCommand { + private let device: MTLDevice + private var cpu: Double = 0.0 + private var gpu: Double = 0.0 + + init(device: MTLDevice) { + // Hold on to the device so we can use it later, when the command is executed. + self.device = device + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + if #available(macOS 11.0, *) { + // Sample CPU and GPU timestamps "from the same moment in time", according to sampleTimestamps() docs. + let (cpuSample, gpuSample) = device.sampleTimestamps() + + // Convert the CPU timestamp to seconds. + // According to docs, the CPU timestamp is "nanoseconds for a point in absolute time or Mach absolute time", + // Which I think makes it comparable to what we have in mglGetSecs (in Matlab) and mglSecs (in this app). + // - https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/converting_gpu_timestamps_into_cpu_time#3730882 + // - https://developer.apple.com/documentation/metal/mtltimestamp?changes=_3 + cpu = Double(cpuSample) * 1e-9 + + // Leave the gpu timestamp as-is (assuming it fits in a double). + gpu = Double(gpuSample) + } + return true + } + + override func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + _ = commandInterface.writeDouble(data: cpu) + _ = commandInterface.writeDouble(data: gpu) + return true + } +} diff --git a/metal/mglMetal/commands/mglSelectStencilCommand.swift b/metal/mglMetal/commands/mglSelectStencilCommand.swift new file mode 100644 index 00000000..b3c23f3e --- /dev/null +++ b/metal/mglMetal/commands/mglSelectStencilCommand.swift @@ -0,0 +1,38 @@ +// +// mglStartStencilCreationCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglSelectStencilCommand : mglCommand { + private let stencilNumber: UInt32 + + init(stencilNumber: UInt32) { + self.stencilNumber = stencilNumber + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface) { + guard let stencilNumber = commandInterface.readUInt32() else { + return nil + } + self.stencilNumber = stencilNumber + super.init(framesRemaining: 1) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + return depthStencilState.selectStencil(view: view, renderEncoder: renderEncoder, stencilNumber: stencilNumber) + } +} diff --git a/metal/mglMetal/commands/mglSetClearColorCommand.swift b/metal/mglMetal/commands/mglSetClearColorCommand.swift new file mode 100644 index 00000000..2e9b5c4d --- /dev/null +++ b/metal/mglMetal/commands/mglSetClearColorCommand.swift @@ -0,0 +1,38 @@ +// +// mglSetClearColorCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 12/22/23. +// Copyright © 2023 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglSetClearColorCommand : mglCommand { + private let clearColor: MTLClearColor! + + init(red: Double, green: Double, blue: Double, alpha: Double = 1.0) { + clearColor = MTLClearColor(red: red, green: green, blue: blue, alpha: 1.0) + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface) { + guard let color = commandInterface.readColor() else { + return nil + } + clearColor = MTLClearColor(red: Double(color[0]), green: Double(color[1]), blue: Double(color[2]), alpha: 1) + super.init(framesRemaining: 1) + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + view.clearColor = clearColor + return true + } +} diff --git a/metal/mglMetal/commands/mglSetRenderTargetCommand.swift b/metal/mglMetal/commands/mglSetRenderTargetCommand.swift new file mode 100644 index 00000000..4f1bd089 --- /dev/null +++ b/metal/mglMetal/commands/mglSetRenderTargetCommand.swift @@ -0,0 +1,43 @@ +// +// mglSetRenderTargetCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglSetRenderTargetCommand : mglCommand { + let textureNumber: UInt32 + + init(textureNumber: UInt32) { + self.textureNumber = textureNumber + super.init() + } + + init?(commandInterface: mglCommandInterface) { + guard let textureNumber = commandInterface.readUInt32() else { + return nil + } + self.textureNumber = textureNumber + super.init() + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + guard let targetTexture = colorRenderingState.getTexture(textureNumber: textureNumber) else { + logger.info(component: "mglSetRenderTargetCommand", details: "For textureNumber \(textureNumber), choosing onscreen rendering.") + return colorRenderingState.setOnscreenRenderingTarget() + } + + logger.info(component: "mglSetRenderTargetCommand", details: "For textureNumber \(textureNumber), choosing offscreen rendering to texture.") + return colorRenderingState.setRenderTarget(view: view, targetTexture: targetTexture) + } +} diff --git a/metal/mglMetal/commands/mglSetViewColorPixelFormatCommand.swift b/metal/mglMetal/commands/mglSetViewColorPixelFormatCommand.swift new file mode 100644 index 00000000..f1951b68 --- /dev/null +++ b/metal/mglMetal/commands/mglSetViewColorPixelFormatCommand.swift @@ -0,0 +1,44 @@ +// +// mglSetViewColorPixelFormatCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglSetViewColorPixelFormatCommand : mglCommand { + private var format: MTLPixelFormat = .bgra8Unorm + + init(format: MTLPixelFormat) { + self.format = format + super.init() + } + + init?(commandInterface: mglCommandInterface) { + guard let formatIndex = commandInterface.readUInt32() else { + return nil + } + switch formatIndex { + case 0: format = .bgra8Unorm + case 1: format = .bgra8Unorm_srgb + case 2: format = .rgba16Float + case 3: format = .rgb10a2Unorm + case 4: format = .bgr10a2Unorm + default: format = .bgra8Unorm + } + super.init() + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + return colorRenderingState.setOnscreenColorPixelFormat(view: view, pixelFormat: format) + } +} diff --git a/metal/mglMetal/commands/mglSetWindowFrameInDisplayCommand.swift b/metal/mglMetal/commands/mglSetWindowFrameInDisplayCommand.swift new file mode 100644 index 00000000..f3a295cd --- /dev/null +++ b/metal/mglMetal/commands/mglSetWindowFrameInDisplayCommand.swift @@ -0,0 +1,82 @@ +// +// mglFullscreenCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglSetWindowFrameInDisplayCommand : mglCommand { + private let displayNumber: UInt32 + private let windowX: UInt32 + private let windowY: UInt32 + private let windowWidth: UInt32 + private let windowHeight: UInt32 + + init(displayNumber: UInt32, windowX: UInt32, windowY: UInt32, windowWidth: UInt32, windowHeight: UInt32) { + self.displayNumber = displayNumber + self.windowX = windowX + self.windowY = windowY + self.windowWidth = windowWidth + self.windowHeight = windowHeight + super.init() + } + + init?(commandInterface: mglCommandInterface) { + guard let displayNumber = commandInterface.readUInt32(), + let windowX = commandInterface.readUInt32(), + let windowY = commandInterface.readUInt32(), + let windowWidth = commandInterface.readUInt32(), + let windowHeight = commandInterface.readUInt32() else { + return nil + } + self.displayNumber = displayNumber + self.windowX = windowX + self.windowY = windowY + self.windowWidth = windowWidth + self.windowHeight = windowHeight + super.init() + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + // Convert Matlab's 1-based display number to a zero-based screen index. + let screenIndex = displayNumber == 0 ? Array.Index(0) : Array.Index(displayNumber - 1) + + // Location of the chosen display AKA screen, according to the system desktop manager. + // Units might be hi-res "points", convert to native display pixels AKA "backing" as needed. + let screens = NSScreen.screens + let screen = screens.indices.contains(screenIndex) ? screens[screenIndex] : screens[0] + let screenNativeFrame = screen.convertRectToBacking(screen.frame) + + // Location of the window relative to the chosen display, in native pixels + let x = Int(screenNativeFrame.origin.x) + Int(windowX) + let y = Int(screenNativeFrame.origin.y) + Int(windowY) + let windowNativeFrame = NSRect(x: x, y: y, width: Int(windowWidth), height: Int(windowHeight)) + + // Location of the window in hi-res "points", or whatever, depending on system config. + let windowScreenFrame = screen.convertRectFromBacking(windowNativeFrame) + + guard let window = view.window else { + logger.error(component: "mglSetWindowFrameInDisplayCommand", details: "Could not get window from view, skipping set window frame command.") + return false + } + + if window.styleMask.contains(.fullScreen) { + logger.info(component: "mglSetWindowFrameInDisplayCommand", details: "App is fullscreen, skipping set window frame command.") + return false + } + + logger.info(component: "mglSetWindowFrameInDisplayCommand", details: "Setting window to display \(displayNumber) frame \(String(describing: windowScreenFrame)).") + window.setFrame(windowScreenFrame, display: true) + return true + } +} diff --git a/metal/mglMetal/commands/mglSetXformCommand.swift b/metal/mglMetal/commands/mglSetXformCommand.swift new file mode 100644 index 00000000..00cee503 --- /dev/null +++ b/metal/mglMetal/commands/mglSetXformCommand.swift @@ -0,0 +1,44 @@ +// +// mglSetXformCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglSetXformCommand : mglCommand { + private let deg2metal: simd_float4x4 + + init(deg2metal: simd_float4x4) { + self.deg2metal = deg2metal + super.init(framesRemaining: 1) + } + + init?(commandInterface: mglCommandInterface) { + guard let deg2metal = commandInterface.readXform() else { + return nil + } + self.deg2metal = deg2metal + super.init(framesRemaining: 1) + } + + override func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + // Update the app state to use this transform on subsequent render passes / frames. + deg2metal = self.deg2metal + + // Update the current render pass to use the same transform on this frame. + // Using index 1 is our convention, expected by all our vertex shaders. + renderEncoder.setVertexBytes(°2metal, length: MemoryLayout.stride, index: 1) + return true + } +} diff --git a/metal/mglMetal/commands/mglStartStencilCreationCommand.swift b/metal/mglMetal/commands/mglStartStencilCreationCommand.swift new file mode 100644 index 00000000..4e22e81d --- /dev/null +++ b/metal/mglMetal/commands/mglStartStencilCreationCommand.swift @@ -0,0 +1,41 @@ +// +// mglStartStencilCreationCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglStartStencilCreationCommand : mglCommand { + private let stencilNumber: UInt32 + private let isInverted: Bool + + init(stencilNumber: UInt32, isInverted: Bool) { + self.stencilNumber = stencilNumber + self.isInverted = isInverted + super.init() + } + + init?(commandInterface: mglCommandInterface) { + guard let stencilNumber = commandInterface.readUInt32(), + let isInverted = commandInterface.readUInt32() else { + return nil + } + self.stencilNumber = stencilNumber + self.isInverted = isInverted != 0 + super.init() + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + return depthStencilState.startStencilCreation(view: view, stencilNumber: stencilNumber, isInverted: isInverted) + } +} diff --git a/metal/mglMetal/commands/mglUpdateTextureCommand.swift b/metal/mglMetal/commands/mglUpdateTextureCommand.swift new file mode 100644 index 00000000..35a5d4fc --- /dev/null +++ b/metal/mglMetal/commands/mglUpdateTextureCommand.swift @@ -0,0 +1,75 @@ +// +// mglUpdateTextureCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglUpdateTextureCommand : mglCommand { + private let textureNumber: UInt32 + private let newTexture: MTLTexture + + init(textureNumber: UInt32, newTexture: MTLTexture) { + self.textureNumber = textureNumber + self.newTexture = newTexture + super.init(framesRemaining: 1) + } + + // This reads the new, incoming texture data into a temporary texture buffer. + // Then later, this copies the new data into the existing texture's buffer. + // We used to copy bytes directly from the command interface to the existing texture's bufffer, and this was nice. + // In order to support queued batches of commands, which are all read in ahead of time before processing / rendering, + // We need to allow some separation in time. + // Otherwise, all queued updates for the same texture would get clobbered by the last update. + init?(commandInterface: mglCommandInterface, device: MTLDevice) { + guard let textureNumber = commandInterface.readUInt32(), + let newTexture = commandInterface.createTexture(device: device) else { + return nil + } + self.textureNumber = textureNumber + self.newTexture = newTexture + super.init(framesRemaining: 1) + } + + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + // Resolve the existing texture and sanity check against the new, incoming texture. + guard let existingTexture = colorRenderingState.getTexture(textureNumber: textureNumber) else { + return false + } + + if (newTexture.width != existingTexture.width || newTexture.height != existingTexture.height) { + logger.error(component: "mglUpdateTextureCommand", details: "Textures are not the same size: new \(newTexture.width) x \(newTexture.height) vs existing \(existingTexture.width) x \(existingTexture.height)") + return false + } + + guard let existingBuffer = existingTexture.buffer else { + logger.error(component: "mglUpdateTextureCommand", details: "Existing texture has no buffer to update: \(String(describing: existingTexture))") + return false + } + + guard let newBuffer = newTexture.buffer else { + logger.error(component: "mglUpdateTextureCommand", details: "New texture has no buffer to update: \(String(describing: newTexture))") + return false + } + + if (newBuffer.allocatedSize != existingBuffer.allocatedSize) { + logger.error(component: "mglUpdateTextureCommand", details: "Texture buffers are not the same size: new \(newBuffer.allocatedSize) vs existing \(existingBuffer.allocatedSize)") + return false + } + + // Copy actual image data from the new, incoming texture to the existing texture, in place. + existingBuffer.contents().copyMemory(from: newBuffer.contents(), byteCount: existingBuffer.allocatedSize) + existingBuffer.didModifyRange(0 ..< existingBuffer.allocatedSize) + return true + } +} diff --git a/metal/mglMetal/commands/mglWindowedCommand.swift b/metal/mglMetal/commands/mglWindowedCommand.swift new file mode 100644 index 00000000..110dfb31 --- /dev/null +++ b/metal/mglMetal/commands/mglWindowedCommand.swift @@ -0,0 +1,35 @@ +// +// mglFullscreenCommand.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/3/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +class mglWindowedCommand : mglCommand { + override func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + NSCursor.unhide() + mglDisplayCursorCommand.cursorHidden = false + + guard let window = view.window else { + logger.error(component: "mglWindowedCommand", details: "Could not get window from view, skipping windowed command.") + return false + } + + if !window.styleMask.contains(.fullScreen) { + logger.info(component: "mglWindowedCommand", details: "App is already windowed, skipping windowed command.") + } else { + window.toggleFullScreen(nil) + } + return true + } +} diff --git a/metal/mglMetal/mglColorRenderingConfig.swift b/metal/mglMetal/mglColorRenderingConfig.swift index c44ddc68..16aa3c62 100644 --- a/metal/mglMetal/mglColorRenderingConfig.swift +++ b/metal/mglMetal/mglColorRenderingConfig.swift @@ -2,33 +2,193 @@ // mglColorRenderingConfig.swift // mglMetal // -// This caputres "things we need to do to set up for color rendering". -// What does that mean? Things like setting up a Metal render pass descriptor -// And managing textures to hold color and/or depth data. -// -// Why is it worth extracthing this out of the mglRenderer? -// Because we have at least two different flavors of color rendering: -// Normal on-screen rendering, and off-screen rendering to a chosen texture. -// -// There are several places in mglRenderer where we do similar-but-differrent things, -// Depending on which flavor of color rendering we're doing. -// This caused too many if-else sections scattered around the code, -// that all needed to work together. -// Better to group all the "ifs" into one place, and the "elses" into another. -// // Created by Benjamin Heasly on 5/2/22. // Copyright © 2022 GRU. All rights reserved. // import Foundation import MetalKit -import os.log +/* + mglColorRenderingState keeps track the current color rendering state for the app, including: + - whether we're rendering to screen or to an offscreen texture + - how to set up a rendering pass for screen or texture + - depth and stencil textures that correspond to the color rendering target + + Color rendering config needs to be applied at a couple of points for each render pass. + At each point we need to be consistent about what state we're in and which texture to target. + mglColorRenderingState encloses the state-dependent consistency with a polymorphic/strategy approach, + which seems nicer than having lots of conditionals in the render pass setup code. + mglRenderer just needs to call mglColorRenderingState methods at the right times. + */ +class mglColorRenderingState { + private let logger: mglLogger + + // The Metal library that holds our compiled shaders. + private let library: MTLLibrary + + // The usual config for on-screen rendering. + private var onscreenRenderingConfig: mglColorRenderingConfig! + + // The current config might be onscreenRenderingConfig, or one targeting a specific texture. + private var currentColorRenderingConfig: mglColorRenderingConfig! + + // A collection of user-managed textures to render to and/or blt to screen. + private var textureSequence = UInt32(1) + private var textures : [UInt32: MTLTexture] = [:] + + init(logger: mglLogger, device: MTLDevice, view: MTKView) { + self.logger = logger + + guard let library = device.makeDefaultLibrary() else { + fatalError("Could not create Metal shader library!") + } + self.library = library + + // Default to onscreen rendering config. + guard let onscreenRenderingConfig = mglOnscreenRenderingConfig( + logger: logger, + device: device, + library: library, + view: view + ) else { + fatalError("Could not create onscreen rendering config, got nil!") + } + self.onscreenRenderingConfig = onscreenRenderingConfig + self.currentColorRenderingConfig = onscreenRenderingConfig + } + + // Collaborate with mglRenderer to set up a render pass. + func getRenderPassDescriptor(view: MTKView) -> MTLRenderPassDescriptor? { + return currentColorRenderingConfig.getRenderPassDescriptor(view: view) + } + + // Collaborate with mglRenderer to set up a render pass. + func finishDrawing(commandBuffer: MTLCommandBuffer, drawable: CAMetalDrawable) { + return currentColorRenderingConfig.finishDrawing(commandBuffer: commandBuffer, drawable: drawable) + } + + // Collaborate with mglRenderer to set up a render pass. + func getDotsPipelineState() -> MTLRenderPipelineState { + return currentColorRenderingConfig.dotsPipelineState + } + + // Collaborate with mglRenderer to set up a render pass. + func getArcsPipelineState() -> MTLRenderPipelineState { + return currentColorRenderingConfig.arcsPipelineState + } + + // Collaborate with mglRenderer to set up a render pass. + func getTexturePipelineState() -> MTLRenderPipelineState { + return currentColorRenderingConfig.texturePipelineState + } + + // Collaborate with mglRenderer to set up a render pass. + func getVerticesWithColorPipelineState() -> MTLRenderPipelineState { + return currentColorRenderingConfig.verticesWithColorPipelineState + } + + // Let mglRenderer grab the current fame from a texture target. + func frameGrab() -> (width: Int, height: Int, pointer: UnsafeMutablePointer?) { + return currentColorRenderingConfig.frameGrab() + } + + // Select a pixel format for onscreen rendering. + func setOnscreenColorPixelFormat(view: MTKView, pixelFormat: MTLPixelFormat) -> Bool { + view.colorPixelFormat = pixelFormat + + // Recreate the onscreen color rendering config so that render pipelines will use the new color pixel format. + guard let device = view.device, + let newOnscreenRenderingConfig = mglOnscreenRenderingConfig( + logger: logger, + device: device, + library: library, + view: view + ) else { + logger.error(component: "mglColorRenderingState", details: "Could not create onscreen rendering config for pixel format \(String(describing: view.colorPixelFormat)).") + return false + } + + if (self.currentColorRenderingConfig is mglOnscreenRenderingConfig) { + // Start using the new config right away! + self.currentColorRenderingConfig = newOnscreenRenderingConfig + } + + // Remember the new onscreen config for later, even if we're currently rendering offscreen. + self.onscreenRenderingConfig = newOnscreenRenderingConfig + + return true + } + + // Default back to onscreen rendering. + func setOnscreenRenderingTarget() -> Bool { + currentColorRenderingConfig = onscreenRenderingConfig + return true + } + + // Use the given texture as an offscreen rendering target. + func setRenderTarget(view: MTKView, targetTexture: MTLTexture) -> Bool { + guard let device = view.device, + let newTextureRenderingConfig = mglOffScreenTextureRenderingConfig( + logger: logger, + device: device, + library: library, + view: view, + texture: targetTexture + ) else { + logger.error(component: "mglColorRenderingState", details: "Could not create offscreen rendering config, got nil.") + return false + } + currentColorRenderingConfig = newTextureRenderingConfig + return true + } + + // Report the size of the onscreen drawable or offscreen texture. + func getSize(view: MTKView) -> (Float, Float) { + return currentColorRenderingConfig.getSize(view: view) + } -// This declares the operations that mglRenderer relies on, -// to set up Metal rendering passes and pipelines. + // Add a new texture to the available blt sources and render targets. + func addTexture(texture: MTLTexture) -> UInt32 { + // Consume a texture number from the bookkeeping sequence. + let consumedTextureNumber = textureSequence + textures[consumedTextureNumber] = texture + textureSequence += 1 + return consumedTextureNumber + } + + // Get an existing texture from the collection, if one exists with the given number. + func getTexture(textureNumber: UInt32) -> MTLTexture? { + guard let texture = textures[textureNumber] else { + logger.error(component: "mglColorRenderingState", details: "Can't get invalid texture number \(textureNumber), valid numbers are \(String(describing: textures.keys))") + return nil + } + return texture + } + + // Remove and return an existing texture from the collection, if one exists with the given number. + func removeTexture(textureNumber: UInt32) -> MTLTexture? { + guard let texture = textures.removeValue(forKey: textureNumber) else { + logger.error(component: "mglColorRenderingState", details: "Can't remove invalid texture number \(textureNumber), valid numbers are \(String(describing: textures.keys))") + return nil + } + + logger.info(component: "mglColorRenderingState", details: "Removed texture number \(textureNumber), remaining numbers are \(String(describing: textures.keys))") + return texture + } + + func getTextureCount() -> UInt32 { + return UInt32(textures.count) + } + + func getTextureNumbers() -> Array { + return Array(textures.keys).sorted() + } +} + +// This declares the operations that mglRenderer relies on to set up Metal rendering passes and pipelines. // It will have different implementations for on-screen vs off-screen rendering. -protocol mglColorRenderingConfig { +private protocol mglColorRenderingConfig { var dotsPipelineState: MTLRenderPipelineState { get } var arcsPipelineState: MTLRenderPipelineState { get } var verticesWithColorPipelineState: MTLRenderPipelineState { get } @@ -41,41 +201,42 @@ protocol mglColorRenderingConfig { func frameGrab()->(width: Int, height: Int, pointer: UnsafeMutablePointer?) } -class mglOnscreenRenderingConfig : mglColorRenderingConfig { +private class mglOnscreenRenderingConfig : mglColorRenderingConfig { + private let logger: mglLogger let dotsPipelineState: MTLRenderPipelineState let arcsPipelineState: MTLRenderPipelineState let verticesWithColorPipelineState: MTLRenderPipelineState let texturePipelineState: MTLRenderPipelineState - init?(device: MTLDevice, library: MTLLibrary, view: MTKView) { - // Until an explicit OOP command model exists, we can just call static functions of mglRenderer. + init?(logger: mglLogger, device: MTLDevice, library: MTLLibrary, view: MTKView) { + self.logger = logger do { dotsPipelineState = try device.makeRenderPipelineState( - descriptor: mglRenderer.dotsPipelineStateDescriptor( + descriptor: dotsPipelineStateDescriptor( colorPixelFormat: view.colorPixelFormat, depthPixelFormat: view.depthStencilPixelFormat, stencilPixelFormat: view.depthStencilPixelFormat, library: library)) arcsPipelineState = try device.makeRenderPipelineState( - descriptor: mglRenderer.arcsPipelineStateDescriptor( + descriptor: arcsPipelineStateDescriptor( colorPixelFormat: view.colorPixelFormat, depthPixelFormat: view.depthStencilPixelFormat, stencilPixelFormat: view.depthStencilPixelFormat, library: library)) verticesWithColorPipelineState = try device.makeRenderPipelineState( - descriptor: mglRenderer.drawVerticesPipelineStateDescriptor( + descriptor: drawVerticesPipelineStateDescriptor( colorPixelFormat: view.colorPixelFormat, depthPixelFormat: view.depthStencilPixelFormat, stencilPixelFormat: view.depthStencilPixelFormat, library: library)) texturePipelineState = try device.makeRenderPipelineState( - descriptor: mglRenderer.bltTexturePipelineStateDescriptor( + descriptor: bltTexturePipelineStateDescriptor( colorPixelFormat: view.colorPixelFormat, depthPixelFormat: view.depthStencilPixelFormat, stencilPixelFormat: view.depthStencilPixelFormat, library: library)) } catch let error { - os_log("Could not create onscreen pipeline state: %@", log: .default, type: .error, String(describing: error)) + logger.error(component: "mglOnscreenRenderingConfig", details: "Could not create onscreen pipeline state: \(String(describing: error))") return nil } } @@ -89,8 +250,6 @@ class mglOnscreenRenderingConfig : mglColorRenderingConfig { } func finishDrawing(commandBuffer: MTLCommandBuffer, drawable: CAMetalDrawable) { - commandBuffer.present(drawable) - commandBuffer.commit() } // frameGrab, since everything is being drawn to a CAMetalDrawable, it does not @@ -98,13 +257,14 @@ class mglOnscreenRenderingConfig : mglColorRenderingConfig { // has to be drawn into an offscreen texture, so for now, this function just returns // nil to notify that the frameGrab is impossible func frameGrab() -> (width: Int, height: Int, pointer: UnsafeMutablePointer?) { - os_log("(mglColorRenderingConfig:frameGrab) Cannot get frame because render target is the screen", log: .default, type: .error) - return (0,0,nil) + logger.error(component: "mglOnscreenRenderingConfig", details: "Cannot get frame because render target is the screen") + return (0,0,nil) } } -class mglOffScreenTextureRenderingConfig : mglColorRenderingConfig { +private class mglOffScreenTextureRenderingConfig : mglColorRenderingConfig { + private let logger: mglLogger let dotsPipelineState: MTLRenderPipelineState let arcsPipelineState: MTLRenderPipelineState let verticesWithColorPipelineState: MTLRenderPipelineState @@ -114,7 +274,8 @@ class mglOffScreenTextureRenderingConfig : mglColorRenderingConfig { let depthStencilTexture: MTLTexture let renderPassDescriptor: MTLRenderPassDescriptor - init?(device: MTLDevice, library: MTLLibrary, view: MTKView, texture: MTLTexture) { + init?(logger: mglLogger, device: MTLDevice, library: MTLLibrary, view: MTKView, texture: MTLTexture) { + self.logger = logger self.colorTexture = texture let depthStencilTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor( @@ -125,7 +286,7 @@ class mglOffScreenTextureRenderingConfig : mglColorRenderingConfig { depthStencilTextureDescriptor.storageMode = .private depthStencilTextureDescriptor.usage = .renderTarget guard let depthStencilTexture = device.makeTexture(descriptor: depthStencilTextureDescriptor) else { - os_log("Could not create offscreen depth-and-stencil texture, got nil!", log: .default, type: .error) + logger.error(component: "mglOffScreenTextureRenderingConfig", details: "Could not create offscreen depth-and-stencil texture, got nil!") return nil } self.depthStencilTexture = depthStencilTexture @@ -141,31 +302,31 @@ class mglOffScreenTextureRenderingConfig : mglColorRenderingConfig { do { dotsPipelineState = try device.makeRenderPipelineState( - descriptor: mglRenderer.dotsPipelineStateDescriptor( + descriptor: dotsPipelineStateDescriptor( colorPixelFormat: texture.pixelFormat, depthPixelFormat: view.depthStencilPixelFormat, stencilPixelFormat: view.depthStencilPixelFormat, library: library)) arcsPipelineState = try device.makeRenderPipelineState( - descriptor: mglRenderer.arcsPipelineStateDescriptor( + descriptor: arcsPipelineStateDescriptor( colorPixelFormat: texture.pixelFormat, depthPixelFormat: view.depthStencilPixelFormat, stencilPixelFormat: view.depthStencilPixelFormat, library: library)) verticesWithColorPipelineState = try device.makeRenderPipelineState( - descriptor: mglRenderer.drawVerticesPipelineStateDescriptor( + descriptor: drawVerticesPipelineStateDescriptor( colorPixelFormat: texture.pixelFormat, depthPixelFormat: view.depthStencilPixelFormat, stencilPixelFormat: view.depthStencilPixelFormat, library: library)) texturePipelineState = try device.makeRenderPipelineState( - descriptor: mglRenderer.bltTexturePipelineStateDescriptor( + descriptor: bltTexturePipelineStateDescriptor( colorPixelFormat: texture.pixelFormat, depthPixelFormat: view.depthStencilPixelFormat, stencilPixelFormat: view.depthStencilPixelFormat, library: library)) } catch let error { - os_log("(mglColorRenderingConfig) Could not create offscreen pipeline state: %@", log: .default, type: .error, String(describing: error)) + logger.error(component: "mglOffScreenTextureRenderingConfig", details: "Could not create offscreen pipeline state: \(String(describing: error))") return nil } } @@ -181,17 +342,12 @@ class mglOffScreenTextureRenderingConfig : mglColorRenderingConfig { } func finishDrawing(commandBuffer: MTLCommandBuffer, drawable: CAMetalDrawable) { + // Make sure the CPU can read the rendering results when we're done. let bltCommandEncoder = commandBuffer.makeBlitCommandEncoder() bltCommandEncoder?.synchronize(resource: colorTexture) bltCommandEncoder?.endEncoding() - - commandBuffer.present(drawable) - commandBuffer.commit() - - // Wait until the bltCommandEncoder is done syncing data from GPU to CPU. - commandBuffer.waitUntilCompleted() } - + // frameGrab, this will write the bytes of the texture into an array func frameGrab() -> (width: Int, height: Int, pointer: UnsafeMutablePointer?) { // first make sure we have the right MTLTexture format (this should always be the same - it's set in @@ -212,10 +368,181 @@ class mglOffScreenTextureRenderingConfig : mglColorRenderingConfig { } else { // write log message - os_log("(mglColorRenderingConfig:frameGrab) Render target texture is not in rgba32float format", log: .default, type: .error) + logger.error(component: "mglOffScreenTextureRenderingConfig", details: "Cannot get frame because render target texture is not in rgba32float format") // could not get bytes, return 0,0,nil return (0,0,nil) } } } + +// Create the config for drawing with our mgl "dots" shaders. +// This depends on whether we're rendering to screen or to offscreen texture. +private func dotsPipelineStateDescriptor( + colorPixelFormat: MTLPixelFormat, + depthPixelFormat: MTLPixelFormat, + stencilPixelFormat: MTLPixelFormat, + library: MTLLibrary? +) -> MTLRenderPipelineDescriptor { + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.depthAttachmentPixelFormat = depthPixelFormat + pipelineDescriptor.stencilAttachmentPixelFormat = stencilPixelFormat + pipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat + pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.sourceAlpha; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.sourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; + + let vertexDescriptor = MTLVertexDescriptor() + vertexDescriptor.attributes[0].format = .float3 + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + vertexDescriptor.attributes[1].format = .float4 + vertexDescriptor.attributes[1].offset = 3 * MemoryLayout.size + vertexDescriptor.attributes[1].bufferIndex = 0 + vertexDescriptor.attributes[2].format = .float2 + vertexDescriptor.attributes[2].offset = 7 * MemoryLayout.size + vertexDescriptor.attributes[2].bufferIndex = 0 + vertexDescriptor.attributes[3].format = .float + vertexDescriptor.attributes[3].offset = 9 * MemoryLayout.size + vertexDescriptor.attributes[3].bufferIndex = 0 + vertexDescriptor.attributes[4].format = .float + vertexDescriptor.attributes[4].offset = 10 * MemoryLayout.size + vertexDescriptor.attributes[4].bufferIndex = 0 + vertexDescriptor.layouts[0].stride = 11 * MemoryLayout.size + pipelineDescriptor.vertexDescriptor = vertexDescriptor + pipelineDescriptor.vertexFunction = library?.makeFunction(name: "vertex_dots") + pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "fragment_dots") + + return pipelineDescriptor +} + +// Create the config for drawing with our mgl "arcs" shaders. +// This depends on whether we're rendering to screen or to offscreen texture. +private func arcsPipelineStateDescriptor( + colorPixelFormat: MTLPixelFormat, + depthPixelFormat: MTLPixelFormat, + stencilPixelFormat: MTLPixelFormat, + library: MTLLibrary? +) -> MTLRenderPipelineDescriptor { + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.depthAttachmentPixelFormat = depthPixelFormat + pipelineDescriptor.stencilAttachmentPixelFormat = stencilPixelFormat + pipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat + pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.sourceAlpha; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.sourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; + + let vertexDescriptor = MTLVertexDescriptor() + // xyz + vertexDescriptor.attributes[0].format = .float3 + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + // rgba + vertexDescriptor.attributes[1].format = .float4 + vertexDescriptor.attributes[1].offset = 3 * MemoryLayout.stride + vertexDescriptor.attributes[1].bufferIndex = 0 + // radii + vertexDescriptor.attributes[2].format = .float4 + vertexDescriptor.attributes[2].offset = 7 * MemoryLayout.stride + vertexDescriptor.attributes[2].bufferIndex = 0 + // wedge + vertexDescriptor.attributes[3].format = .float2 + vertexDescriptor.attributes[3].offset = 11 * MemoryLayout.stride + vertexDescriptor.attributes[3].bufferIndex = 0 + // border + vertexDescriptor.attributes[4].format = .float + vertexDescriptor.attributes[4].offset = 13 * MemoryLayout.stride + vertexDescriptor.attributes[4].bufferIndex = 0 + // center vertex (computed) + vertexDescriptor.attributes[5].format = .float3 + vertexDescriptor.attributes[5].offset = 14 * MemoryLayout.stride + vertexDescriptor.attributes[5].bufferIndex = 0 + // viewport size + vertexDescriptor.attributes[6].format = .float2 + vertexDescriptor.attributes[6].offset = 17 * MemoryLayout.stride + vertexDescriptor.attributes[6].bufferIndex = 0 + vertexDescriptor.layouts[0].stride = 19 * MemoryLayout.stride + pipelineDescriptor.vertexDescriptor = vertexDescriptor + pipelineDescriptor.vertexFunction = library?.makeFunction(name: "vertex_arcs") + pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "fragment_arcs") + + return pipelineDescriptor +} + +// Create the config for drawing with our mgl "textures" shaders. +// This depends on whether we're rendering to screen or to offscreen texture. +private func bltTexturePipelineStateDescriptor( + colorPixelFormat: MTLPixelFormat, + depthPixelFormat: MTLPixelFormat, + stencilPixelFormat: MTLPixelFormat, + library: MTLLibrary? +) -> MTLRenderPipelineDescriptor { + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.depthAttachmentPixelFormat = depthPixelFormat + pipelineDescriptor.stencilAttachmentPixelFormat = stencilPixelFormat + pipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat + pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.sourceAlpha; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.sourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; + + let vertexDescriptor = MTLVertexDescriptor() + vertexDescriptor.attributes[0].format = .float3 + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + vertexDescriptor.attributes[1].format = .float2 + vertexDescriptor.attributes[1].offset = 3 * MemoryLayout.size + vertexDescriptor.attributes[1].bufferIndex = 0 + vertexDescriptor.layouts[0].stride = 5 * MemoryLayout.size + pipelineDescriptor.vertexDescriptor = vertexDescriptor + pipelineDescriptor.vertexFunction = library?.makeFunction(name: "vertex_textures") + pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "fragment_textures") + + return pipelineDescriptor +} + +// Create the config for drawing with our mgl "color vertices" shaders. +// This depends on whether we're rendering to screen or to offscreen texture. +private func drawVerticesPipelineStateDescriptor( + colorPixelFormat: MTLPixelFormat, + depthPixelFormat: MTLPixelFormat, + stencilPixelFormat: MTLPixelFormat, + library: MTLLibrary? +) -> MTLRenderPipelineDescriptor { + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.depthAttachmentPixelFormat = depthPixelFormat + pipelineDescriptor.stencilAttachmentPixelFormat = stencilPixelFormat + pipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat + pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.sourceAlpha; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.sourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; + + let vertexDescriptor = MTLVertexDescriptor() + vertexDescriptor.attributes[0].format = .float3 + vertexDescriptor.attributes[0].offset = 0 + vertexDescriptor.attributes[0].bufferIndex = 0 + vertexDescriptor.attributes[1].format = .float3 + vertexDescriptor.attributes[1].offset = 3 * MemoryLayout.size + vertexDescriptor.attributes[1].bufferIndex = 0 + vertexDescriptor.layouts[0].stride = 6 * MemoryLayout.size + pipelineDescriptor.vertexDescriptor = vertexDescriptor + pipelineDescriptor.vertexFunction = library?.makeFunction(name: "vertex_with_color") + pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "fragment_with_color") + + return pipelineDescriptor +} diff --git a/metal/mglMetal/mglCommandInterface.swift b/metal/mglMetal/mglCommandInterface.swift index 6e5de1f4..804a5cb4 100644 --- a/metal/mglMetal/mglCommandInterface.swift +++ b/metal/mglMetal/mglCommandInterface.swift @@ -10,86 +10,369 @@ import Foundation import MetalKit -import os.log + +/* + The command interface can be in three states, with respect to command batches. + The command codes startBatch, processBatch, and finishBatch can cycle the command + interface through these states, in order. + */ +enum BatchState { + /* + The default state, "none", is normal operation where commands are processed and + reported to the client one at a time. + In this state socket operations and command processing are interleaved. + */ + case none + + /* + A "startBatch" command code moves the command interface into the "building" state. + This state is focused on socket operations but not command processing. + The client may send multiple commands and these will be fully read as usual, + then added to the todo queue for later processing. + For each command received in this state, the command interface will immediately + report placeholder command results to the client, in order to prevent client + blocking on each command submitted. + In this state awaitNext() will never report commands as available. + */ + case building + + /* + A "processBatch" command code moves the command interface into the "processing" state. + This state is focused on command processing but not socket operations. + In this astate awaitNext() will again expose enqueued todo commands to the renderer, + allowing the commands to be processed, in the order they arrived, as fast as they can. + Completed commands will be added to the done queue, for later reporting to the client. + This keeps the socket quiet during batch processing. + + A "finishBatch" command code moves the command interface back to the "done" state. + As part of this transition it will report all enqueued done commands to the client + in the order received and processed. This reporting will include standard + timestamps for each command, but not any command-specific query results. + This keeps the batch results uniform, which should simplify client code for + handling the batch response. + */ + case processing +} //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // This class combines an mglServer instance with // the header mglCommandTypes.h, which is also used // in our Matlab code, to safely read and write // supported commands and data types. -// -// This opens a connection to Matlab based on a -// connection address passed as a command line option: -// mglMetal ... -mglConnectionAddress my-address //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ class mglCommandInterface { + private let logger: mglLogger + private let server: mglServer - + + // This is used as a queue of commands that have been read fully but not yet processed / rendered. + private var todo = [mglCommand]() + + // This is used as a queue of commands that have been processed but results not yet reported to the client. + private var done = [mglCommand]() + + // What state is the command interface in with respect to batches: none, building, or processing? + private var batchState: BatchState = .none + + // Utility to get system nano time. + let secs = mglSecs() + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // init //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - init() { - // Get the connection address to use from the command line - let arguments = CommandLine.arguments - let optionIndex = arguments.firstIndex(of: "-mglConnectionAddress") ?? -2 - if optionIndex < 0 { - os_log("(mglCommandInterface) No command line option passed for -mglConnectionAddress, using a default address.", log: .default, type: .info) + init(logger: mglLogger, server: mglServer) { + self.logger = logger + self.server = server + } + + // What is the current batch state -- used from tests. + func getBatchState() -> BatchState { + return batchState + } + + func startBatch() -> mglCommand? { + self.batchState = .building + return nil + } + + func processBatch() -> mglCommand? { + self.batchState = .processing + return nil + } + + func finishBatch() -> mglCommand? { + writeBatchResults() + done.removeAll() + self.batchState = .none + return nil + } + + // Wait for the next command from the client, read it fully, and add to the todo queue for processing. + // Require a MTLDevice so that commands can immediately write data to GPU device buffers, with no intermediate. + private func awaitCommand(device: MTLDevice) -> mglCommand? { + // Consume the command code that tells us what to do next. + // This will block until one arrives. + guard let commandCode = readCommandCode() else { + return nil + } + + // Acknowledge command received. + let ackTime = secs.get() + _ = writeDouble(data: ackTime) + + var command: mglCommand? = nil + switch (commandCode) { + // Transition batch state but don't initialize a new command -- these return a nil command. + case mglStartBatch: return startBatch() + case mglProcessBatch: return processBatch() + case mglFinishBatch: return finishBatch() + + // Instantiate a new command by reading it fully from the socket. + // This will block until all command-specific parameters arrive. + case mglPing: command = mglPingCommand() + case mglDrainSystemEvents: command = mglDrainSystemEventsCommand() + case mglFullscreen: command = mglFullscreenCommand() + case mglWindowed: command = mglWindowedCommand() + case mglCreateTexture: command = mglCreateTextureCommand(commandInterface: self, device: device) + case mglReadTexture: command = mglReadTextureCommand(commandInterface: self) + case mglSetRenderTarget: command = mglSetRenderTargetCommand(commandInterface: self) + case mglSetWindowFrameInDisplay: command = mglSetWindowFrameInDisplayCommand(commandInterface: self) + case mglGetWindowFrameInDisplay: command = mglGetWindowFrameInDisplayCommand() + case mglDeleteTexture: command = mglDeleteTextureCommand(commandInterface: self) + case mglSetViewColorPixelFormat: command = mglSetViewColorPixelFormatCommand(commandInterface: self) + case mglStartStencilCreation: command = mglStartStencilCreationCommand(commandInterface: self) + case mglFinishStencilCreation: command = mglFinishStencilCreationCommand() + case mglInfo: command = mglInfoCommand() + case mglGetErrorMessage: command = mglGetErrorMessageCommand() + case mglFrameGrab: command = mglFrameGrabCommand() + case mglMinimize: command = mglMinimizeCommand(commandInterface: self) + case mglDisplayCursor: command = mglDisplayCursorCommand(commandInterface: self) + case mglSampleTimestamps: command = mglSampleTimestampsCommand(device: device) + case mglFlush: command = mglFlushCommand(commandInterface: self) + case mglBltTexture: command = mglBltTextureCommand(commandInterface: self, device: device) + case mglSetXform: command = mglSetXformCommand(commandInterface: self) + case mglDots: command = mglDotsCommand(commandInterface: self, device: device) + case mglLine: command = mglLineCommand(commandInterface: self, device: device) + case mglQuad: command = mglQuadCommand(commandInterface: self, device: device) + case mglPolygon: command = mglPolygonCommand(commandInterface: self, device: device) + case mglArcs: command = mglArcsCommand(commandInterface: self, device: device) + case mglUpdateTexture: command = mglUpdateTextureCommand(commandInterface: self, device: device) + case mglSelectStencil: command = mglSelectStencilCommand(commandInterface: self) + case mglSetClearColor: command = mglSetClearColorCommand(commandInterface: self) + case mglRepeatFlicker: command = mglRepeatFlickerCommand(commandInterface: self) + case mglRepeatBlts: command = mglRepeatBltsCommand(commandInterface: self) + case mglRepeatQuads: command = mglRepeatQuadsCommand(commandInterface: self) + case mglRepeatDots: command = mglRepeatDotsCommand(commandInterface: self) + case mglRepeatFlush: command = mglRepeatFlushCommand(commandInterface: self) + default: + command = nil + } + + // In case of an unknown command or command init? error, + // clear out whatever's left on the socket and return to a known, ready state. + if command == nil { + clearReadData() } - let address = arguments.indices.contains(optionIndex + 1) ? arguments[optionIndex + 1] : "mglMetal.socket" - os_log("(mglCommandInterface) using connection addresss %{public}@", log: .default, type: .info, address) - - // In the future we might inspect the address to decide what kind of server to create, - // like local socket vs internet socket, vs shared memory, etc. - // For now, we always interpret the address as a file system path for a local socket. - server = mglLocalServer(pathToBind: address) + + // Note the command code so we can echo it later. + command?.results.commandCode = commandCode + + // Note when this command was created. + command?.results.ackTime = ackTime + + // When building up a batch, unblock the client by sending immediate placeholder results. + if batchState == .building && command != nil { + writeResults(command: command!, asPlaceholder: true) + } + + // We got a whole command, ready to be processed. + return command } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // waitForClientToConnect - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - func acceptClientConnection() -> Bool { - return server.acceptClientConnection() + + // Read zero or more available commands from the client into the todo queue. + // Don't block waiting for commands. + func readAny(device: MTLDevice) { + // Give the client a chance to connect. + if !server.acceptClientConnection() { + return + } + + switch (batchState) { + case BatchState.building: + // Keep reading commands as long as data is available. + // So we can build up the batch as fast as possible, and not wait for render() calls. + // This always incurs one polling timeout at the end. + while server.dataWaiting() { + let command = awaitCommand(device: device) + if command != nil { + todo.append(command!) + } + } + case BatchState.none: + // Read up to one command after waiting up to the default timeout. + // We might expect a full command here, which might the client a few ms to compute. + // If there is data waiting, we won't incur any timeout. + if server.dataWaiting() { + let command = awaitCommand(device: device) + if command != nil { + todo.append(command!) + } + } + case BatchState.processing: + // Read up to one command after waiting a short timeout. + // We don't expect new, full commands here, but we do want to remain responsive. + // If there is data waiting, we won't incur any timeout. + if server.dataWaiting(timeout: 1) { + let command = awaitCommand(device: device) + if command != nil { + todo.append(command!) + } + } + } } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // dataWaiting - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - func dataWaiting() -> Bool { - return server.dataWaiting() + + // Make sure there's a command in the todo queue, blocking and waiting if necessary. + func awaitNext(device: MTLDevice) { + if todo.count > 0 { + return + } + + let command = awaitCommand(device: device) + if command != nil { + todo.append(command!) + } + } + + // Get the next command out of the todo queue. + func next() -> mglCommand? { + if batchState == BatchState.building { + return nil + } + + if todo.count > 0 { + return todo.removeFirst() + } + return nil + } + + // Collaborate with mglRenderer: this command was fully processed. + func done(command: mglCommand, success: Bool = true) { + command.results.success = success + command.results.processedTime = secs.get() + + if batchState == .processing { + // When processing a batch, hold done command results for later. + done.append(command) + + // At the end of the batch, send the client a heads up. + // This gives the client an event to sync on while waiting for batch completion. + // This also lets the client know how many command results to expect. + if todo.isEmpty { + _ = writeUInt32(data: UInt32(done.count)) + } + } else { + // Otherwise, report results immediately. + writeResults(command: command) + } + } + + // Add a command directly to the end of the unprocessed queue -- used during testing. + func addLast(command: mglCommand) { + command.results.ackTime = secs.get() + todo.append(command) + } + + // Add a command directly to the front of the unprocessed queue -- supports repeated commands. + func addNext(command: mglCommand) { + todo.insert(command, at: 0) + } + + // Report command-specific results and timestamps. + private func writeResults(command: mglCommand, asPlaceholder: Bool = false) { + // Write command-specific query results, if any. + _ = command.writeQueryResults(logger: logger, commandInterface: self) + + // Echo the command code. + _ = writeCommand(data: command.results.commandCode) + + // Report an explicit status and the processed time, + // which also represents error status as a negative timestamp. + if command.results.success || asPlaceholder { + _ = writeUInt32(data: 1) + _ = writeDouble(data: command.results.processedTime) + } else { + _ = writeUInt32(data: 0) + logger.error(component: "mglCommandInterface", details: "Command failed: \(String(describing: command))") + _ = writeDouble(data: -command.results.processedTime) + } + + // Report additional, detailed timestamps. + _ = writeDouble(data: command.results.vertexStart) + _ = writeDouble(data: command.results.vertexEnd) + _ = writeDouble(data: command.results.fragmentStart) + _ = writeDouble(data: command.results.fragmentEnd) + _ = writeDouble(data: command.results.drawableAcquired) + _ = writeDouble(data: command.results.drawablePresented) + } + + // Report generic results and timestamps for a command batch. + // This omits any command-specific query results. + // This writes one field at a time across all commands, + // hopefully allowing the client to read in a "vectorized" fashion. + private func writeBatchResults() { + // Echo the command codes. + for command in done { _ = writeCommand(data: command.results.commandCode) } + + // Report explicit statuses. + for command in done { _ = writeUInt32(data: command.results.success ? 1 : 0) } + + // Report processed times, which also represents error status as negatives. + for command in done { + let timestamp = command.results.success ? command.results.processedTime : -command.results.processedTime + _ = writeDouble(data: timestamp) + } + + // Report additional, detailed timestamps. + for command in done { _ = writeDouble(data: command.results.vertexStart) } + for command in done { _ = writeDouble(data: command.results.vertexEnd) } + for command in done { _ = writeDouble(data: command.results.fragmentStart) } + for command in done { _ = writeDouble(data: command.results.fragmentEnd) } + for command in done { _ = writeDouble(data: command.results.drawableAcquired) } + for command in done { _ = writeDouble(data: command.results.drawablePresented) } } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // clearReadData //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - func clearReadData() { + private func clearReadData() { // declare a byte to dump var dumpByte = 0 var numBytes = 0; // while there is data reading - while dataWaiting() { + while server.dataWaiting() { // read a byte let bytesRead = server.readData(buffer: &dumpByte, expectedByteCount: 1) //keep how many bytes we have read numBytes = numBytes+bytesRead; } // display how much data we read. - os_log("(mglCommandInterface:clearReadData) Dumped %{public}d bytes", log: .default, type: .info, numBytes); + logger.info(component: "mglCommandInterface", details: "clearReadData dumped \(numBytes) bytes") } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // readCommand //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - func readCommand() -> mglCommandCode? { + func readCommandCode() -> mglCommandCode? { var data = mglUnknownCommand let expectedByteCount = MemoryLayout.size let bytesRead = server.readData(buffer: &data, expectedByteCount: expectedByteCount) if (bytesRead != expectedByteCount) { - os_log("(mglCommandInterface) Expeted to read command code ${public}d bytes but read %{public}d.", log: .default, type: .error, expectedByteCount, bytesRead) + logger.error(component: "mglCommandInterface", details: "Expeted to read command code \(expectedByteCount) bytes but read \(bytesRead)") return nil } return data } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // readUINT32 //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -98,18 +381,18 @@ class mglCommandInterface { let expectedByteCount = MemoryLayout.size let bytesRead = server.readData(buffer: &data, expectedByteCount: expectedByteCount) if (bytesRead != expectedByteCount) { - os_log("(mglCommandInterface) Expeted to read uint32 ${public}d bytes but read %{public}d.", log: .default, type: .error, expectedByteCount, bytesRead) + logger.error(component: "mglCommandInterface", details: "Expeted to read uint32 \(expectedByteCount) bytes but read \(bytesRead)") return nil } return data } - + func writeUInt32(data: mglUInt32) -> Int { var localData = data let expectedByteCount = MemoryLayout.size return server.sendData(buffer: &localData, byteCount: expectedByteCount) } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // readFloat //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -118,12 +401,12 @@ class mglCommandInterface { let expectedByteCount = MemoryLayout.size let bytesRead = server.readData(buffer: &data, expectedByteCount: expectedByteCount) if (bytesRead != expectedByteCount) { - os_log("(mglCommandInterface) Expeted to read float ${public}d bytes but read %{public}d.", log: .default, type: .error, expectedByteCount, bytesRead) + logger.error(component: "mglCommandInterface", details: "Expeted to read float \(expectedByteCount) bytes but read \(bytesRead)") return nil } return data } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // readColor //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -132,18 +415,18 @@ class mglCommandInterface { defer { data.deallocate() } - + let expectedByteCount = Int(mglSizeOfFloatRgbColor()) let bytesRead = server.readData(buffer: data, expectedByteCount: expectedByteCount) if (bytesRead != expectedByteCount) { - os_log("(mglCommandInterface) Expeted to read rgb color ${public}d bytes but read %{public}d.", log: .default, type: .error, expectedByteCount, bytesRead) + logger.error(component: "mglCommandInterface", details: "Expeted to read rgb color \(expectedByteCount) bytes but read \(bytesRead)") return nil } - + // Let the library decide how simd vectors are packed. return simd_make_float3(data[0], data[1], data[2]) } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // readXform //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -152,14 +435,14 @@ class mglCommandInterface { defer { data.deallocate() } - + let expectedByteCount = Int(mglSizeOfFloat4x4Matrix()) let bytesRead = server.readData(buffer: data, expectedByteCount: expectedByteCount) if (bytesRead != expectedByteCount) { - os_log("(mglCommandInterface) Expeted to read 4x4 float ${public}d bytes but read %{public}d.", log: .default, type: .error, expectedByteCount, bytesRead) + logger.error(component: "mglCommandInterface", details: "Expeted to read 4x4 float \(expectedByteCount) bytes but read \(bytesRead)") return nil } - + // Let the library decide how simd vectors are packed. let column0 = simd_make_float4(data[0], data[1], data[2], data[3]) let column1 = simd_make_float4(data[4], data[5], data[6], data[7]) @@ -167,7 +450,7 @@ class mglCommandInterface { let column3 = simd_make_float4(data[12], data[13], data[14], data[15]) return(simd_float4x4(column0, column1, column2, column3)) } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // readVertices //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -175,30 +458,30 @@ class mglCommandInterface { guard let vertexCount = readUInt32() else { return nil } - + // Calculate how many floats we have per vertex. // Start with 3 for XYZ, plus extraVals which can be used for things like color channels or texture coordinates. let valsPerVertex = mglUInt32(3 + extraVals) let expectedByteCount = Int(mglSizeOfFloatVertexArray(vertexCount, valsPerVertex)) - + // get an MTLBuffer from the GPU // With storageModeManaged, we must explicitly sync the data to the GPU, below. guard let vertexBuffer = device.makeBuffer(length: expectedByteCount, options: .storageModeManaged) else { - os_log("(mglCommandInterface) Could not make vertex buffer of size %{public}d", log: .default, type: .error, expectedByteCount) + logger.error(component: "mglCommandInterface", details: "Could not make vertex buffer of size \(expectedByteCount)") return nil } - + let bytesRead = server.readData(buffer: vertexBuffer.contents(), expectedByteCount: expectedByteCount) if (bytesRead != expectedByteCount) { - os_log("(mglCommandInterface) Expected to read vertex buffer of size %{public}d but read %{public}d", log: .default, type: .error, expectedByteCount, bytesRead) + logger.error(component: "mglCommandInterface", details: "Expected to read vertex buffer of size \(expectedByteCount) but read \(bytesRead)") return nil } - + // With storageModeManaged above, we must explicitly sync the new data to the GPU. vertexBuffer.didModifyRange( 0 ..< expectedByteCount) return (vertexBuffer, Int(vertexCount)) } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // readTexture //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -209,84 +492,84 @@ class mglCommandInterface { guard let textureHeight = readUInt32() else { return nil } - + // Set the texture descriptor let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( pixelFormat: .rgba32Float, width: Int(textureWidth), height: Int(textureHeight), mipmapped: false) - + // For now, all textures can receive rendering output. textureDescriptor.usage = [.renderTarget, .shaderRead, .shaderWrite] - + // Get the size in bytes of each row of the actual incoming image. let imageRowByteCount = Int(mglSizeOfFloatRgbaTexture(mglUInt32(textureWidth), 1)) - + // "Round up" this row size to the next multiple of the system-dependent required alignment (perhaps 16 or 256). let rowAlignment = device.minimumLinearTextureAlignment(for: textureDescriptor.pixelFormat) let alignedRowByteCount = ((imageRowByteCount + rowAlignment - 1) / rowAlignment) * rowAlignment - + // jg: for debugging //os_log("(mglCommandInterface:createTexture) minimumLinearTextureAlignment: %{public}d imageRowByteCount: %{public}d alignedRowByteCount: %{public}d", log: .default, type: .info, rowAlignment, imageRowByteCount, alignedRowByteCount) - + // Get an MTLBuffer from the GPU to store image data in // Use the rounded-up/aligned row size instead of the nominal image size. // With storageModeManaged, we must explicitly sync the data to the GPU, below. let bufferByteSize = alignedRowByteCount * Int(textureHeight) guard let textureBuffer = device.makeBuffer(length: bufferByteSize, options: .storageModeManaged) else { - os_log("(mglCommandInterface) Could not make texture buffer of size %{public}d image width %{public}d aligned buffer width %{public}d and image height %{public}d", log: .default, type: .error, bufferByteSize, textureWidth, alignedRowByteCount, textureHeight) + logger.error(component: "mglCommandInterface", details: "Could not make texture buffer of size \(bufferByteSize) image width \(textureWidth) aligned buffer width \(alignedRowByteCount) and image height \(textureHeight)") return nil } - + // Read from the socket into the texture memory. // Copy image rows one at a time, only taking the nominal row size and leaving the rest of the buffer row as padding. let bytesRead = imageRowsToBuffer(buffer: textureBuffer, imageRowByteCount: imageRowByteCount, alignedRowByteCount: alignedRowByteCount, rowCount: Int(textureHeight)) let expectedByteCount = imageRowByteCount * Int(textureHeight) if (bytesRead != expectedByteCount) { - os_log("(mglCommandInterface) Could not read expected bytes %{public}d for texture, read %{public}d", log: .default, type: .error, expectedByteCount, bytesRead) + logger.error(component: "mglCommandInterface", details: "Could not read expected bytes \(expectedByteCount) for texture, read \(bytesRead)") return nil } - + // Now make the buffer into a texture. guard let texture = textureBuffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: alignedRowByteCount) else { - os_log("(mglCommandInterface) Could not make texture from texture buffer of size %{public}d image width %{public}d aligned buffer width %{public}d and image height %{public}d", log: .default, type: .error, bufferByteSize, textureWidth, alignedRowByteCount, textureHeight) + logger.error(component: "mglCommandInterface", details: "Could not make texture from texture buffer of size \(bufferByteSize) image width \(textureWidth) aligned buffer width \(alignedRowByteCount) and image height \(textureHeight)") return nil } - + return(texture) } - + func imageRowsToBuffer(buffer: MTLBuffer, imageRowByteCount: Int, alignedRowByteCount: Int, rowCount: Int) -> Int { var imageBytesRead = 0 for row in 0 ..< rowCount { let bufferRow = buffer.contents().advanced(by: row * alignedRowByteCount) let rowBytesRead = server.readData(buffer: bufferRow, expectedByteCount: imageRowByteCount) if (rowBytesRead != imageRowByteCount) { - os_log("(mglCommandInterface) Expected to read %{public}d bytes but read %{public}d for image row %{public}d of %{public}d", log: .default, type: .error, imageRowByteCount, rowBytesRead, row, rowCount) + logger.error(component: "mglCommandInterface", details: "Expected to read \(imageRowByteCount) bytes but read \(rowBytesRead) for image row \(row) of \(rowCount)") } imageBytesRead += rowBytesRead } - + // With storageModeManaged above, we must explicitly sync the new data to the GPU. buffer.didModifyRange(0 ..< alignedRowByteCount * rowCount) - + return imageBytesRead } - + func imageRowsFromBuffer(buffer: MTLBuffer, imageRowByteCount: Int, alignedRowByteCount: Int, rowCount: Int) -> Int { var imageBytesSent = 0 for row in 0 ..< rowCount { let bufferRow = buffer.contents().advanced(by: row * alignedRowByteCount) let rowBytesSent = server.sendData(buffer: bufferRow, byteCount: imageRowByteCount) if (rowBytesSent != imageRowByteCount) { - os_log("(mglCommandInterface) Expected to send %{public}d bytes but sent %{public}d for image row %{public}d of %{public}d", log: .default, type: .error, imageRowByteCount, rowBytesSent, row, rowCount) + logger.error(component: "mglCommandInterface", details: "Expected to send \(imageRowByteCount) bytes but read \(rowBytesSent) for image row \(row) of \(rowCount)") } imageBytesSent += rowBytesSent } return imageBytesSent } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // writeDouble //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -295,7 +578,7 @@ class mglCommandInterface { let expectedByteCount = MemoryLayout.size return server.sendData(buffer: &localData, byteCount: expectedByteCount) } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // writeString //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -309,7 +592,7 @@ class mglCommandInterface { expectedByteCount = data.count * 2 return bytesSent + server.sendData(buffer: &localData, byteCount: expectedByteCount) } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // writeCommand //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -318,7 +601,7 @@ class mglCommandInterface { let expectedByteCount = MemoryLayout.size return server.sendData(buffer: &localData, byteCount: expectedByteCount) } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // writeDoubleArray //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -332,7 +615,7 @@ class mglCommandInterface { expectedByteCount = data.count * MemoryLayout.size return bytesSent + server.sendData(buffer: &localData, byteCount: expectedByteCount) } - + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // writeUInt8Array //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ diff --git a/metal/mglMetal/mglCommandModel.swift b/metal/mglMetal/mglCommandModel.swift index 42ad77ea..5f422b7f 100644 --- a/metal/mglMetal/mglCommandModel.swift +++ b/metal/mglMetal/mglCommandModel.swift @@ -7,35 +7,79 @@ // import Foundation +import MetalKit -/** - Over in mglRenderer, we're handling all of the socket and rendering flow control, as well as the inner details of rendering commands. - As we go and grow, I think a pattern is emerging among the rendering commands, which we might want to factor out into an explicit OOP command model. - This could help us clarify what we want a "command" to be, and separate the duties between socket stuff, flow control stuff, and inner graphics details. - Doing this might give us confidence that adding or modifying a particular command would not have unintended effects on other commands. - It might also allow user-defined commands to be loaded as runtime plugins, if we go that route, but providing a protocol/interface to write code to. - It might also support future commands that are "close to the metal", which may need to maintain and manage their own state, across several frames. - - As a start, I'll make notes here about the config, state, and operations that seem to might go into each command, things we might want to factor out. - My source for this is the current code in mglRenderer, which has grown to be somewhat long, around 1000 lines. - Later, maybe this file here can become the place where we define an actual Swift protocol called mglCommandModel. - - Many commands seem to: - - have its own, associated command code in mglCommandTypes - - have a few utility functions associated with it, for example to parse numeric params into Swift Enum values. - - have side-effects on the overall mglMetal app with things like fullscreen vs windowed mode, stencil state, clear color, etc. - - be able to create a suitable render pipeline descriptor, for a given set of pixel formats - - be able to configure an instance of itself with any necessary params, given an mglCommandInterface to read from - - report configuration success or failure as a return value - - be able to add itself to a render pass, given the current view and command encoder - - report render pass success or failure as a return value - - be able to write any requested data back to the caller, given an mglCommandInterface to write to - - report data write pass success or failure as a return value - - It might be that commands also should: - - have a counter for how many times to repeat itself as part of a frame sequence - - be able to load configuration (as from a socket) once at the start of a frame sequence, and reuse the config throughout the sequence - - be able to report whether it's in the middle of a repetition sequence - - be able to configure an instance of itself from a regular init() method, *without* an mglCommandInterface, for standalone testing. - - be able to return requested data back to the caller as regular variables, *without* an mglCommandInterface, for standalone testing. +/* + mglCommand factors out a pattern common to all mgl Metal commands. */ +class mglCommand { + // Here is where mglCommandInterface and mglRenderer can record and report what happened to this command. + var results = mglCommandResults() + + // How many frames left to draw for this command instance? + // Most drawing commands would start with with framesRemaining == 1, meaning draw once and move on. + // Drawing commands that start with framesRemaining > 1 support repeated drawing across a number of frames. + // mglRenderer decrements framesRemaining after drawing each fram. + // Non-drawing commands would start with framesRemaining <= 0. + var framesRemaining: Int = 0 + + // Each command shoud provide two ways to init(): + // - directly in memory, for use during tests + // - by reading from an mglCommandInterface and/or writing to an MTLDevice + // The second one works with a connected client and is allowed allowed to fail and return nil. + // Both inits should call up to this as "super.init()" to initialize framesRemaining. + init(framesRemaining: Int = 0) { + self.framesRemaining = framesRemaining + } + + // Get a chance to query and/or modify the app's state. + // Stash any query results / references on the command instance until writeResults() is called. + // Stashing gives us data to inspect during tests, and will keep the socket quiet during batched command runs. + // Return true to indicate success / false for failure. + func doNondrawingWork( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4 + ) -> Bool { + return true + } + + // Write any stashed query results to the command interface, to send them back to the client. + // Return true to indicate success / false for failure. + func writeQueryResults( + logger: mglLogger, + commandInterface : mglCommandInterface + ) -> Bool { + return true + } + + // Do drawing during a render pass. + func draw( + logger: mglLogger, + view: MTKView, + depthStencilState: mglDepthStencilState, + colorRenderingState: mglColorRenderingState, + deg2metal: inout simd_float4x4, + renderEncoder: MTLRenderCommandEncoder + ) -> Bool { + return true + } +} + +/* + Holder for status and timing around each command, to be set by mglCommandInterface and mglRenderer. + */ +struct mglCommandResults { + var commandCode: mglCommandCode = mglUnknownCommand + var success: Bool = false + var ackTime: Double = 0.0 + var processedTime: Double = 0.0 + var vertexStart: Double = 0.0 + var vertexEnd: Double = 0.0 + var fragmentStart: Double = 0.0 + var fragmentEnd: Double = 0.0 + var drawableAcquired: Double = 0.0 + var drawablePresented: Double = 0.0 +} diff --git a/metal/mglMetal/mglCommandTypes.h b/metal/mglMetal/mglCommandTypes.h index 7e966bd0..5a0c81ca 100644 --- a/metal/mglMetal/mglCommandTypes.h +++ b/metal/mglMetal/mglCommandTypes.h @@ -38,6 +38,10 @@ typedef enum mglCommandCode : uint16_t { mglFrameGrab = 20, mglMinimize = 21, mglDisplayCursor = 22, + mglSampleTimestamps = 23, + mglStartBatch = 100, + mglProcessBatch = 101, + mglFinishBatch = 102, mglDrawingCommands = 1000, mglFlush = 1001, mglBltTexture = 1003, @@ -84,6 +88,10 @@ const mglCommandCode mglCommandCodes[] = { mglFrameGrab, mglMinimize, mglDisplayCursor, + mglSampleTimestamps, + mglStartBatch, + mglProcessBatch, + mglFinishBatch, mglFlush, mglBltTexture, mglSetXform, @@ -124,6 +132,10 @@ const char* mglCommandNames[] = { "mglFrameGrab", "mglMinimize", "mglDisplayCursor", + "mglSampleTimestamps", + "mglStartBatch", + "mglProcessBatch", + "mglFinishBatch", "mglFlush", "mglBltTexture", "mglSetXform", diff --git a/metal/mglMetal/mglDepthStencilConfig.swift b/metal/mglMetal/mglDepthStencilConfig.swift index b03853d4..8fc6498b 100644 --- a/metal/mglMetal/mglDepthStencilConfig.swift +++ b/metal/mglMetal/mglDepthStencilConfig.swift @@ -2,35 +2,6 @@ // mglStencilConfig.swift // mglMetal // -// This captures "things we need to do to set up and apply stencils". -// What does that mean? -// Things like choosing whether we're creating vs applying a stencil, -// and which stencil plane we're creating or applying, -// or maybe we're not using a stencil at all. -// -// The Metal API is confusing regarding the config for stencils and depth tests. -// In some parts of the API, these are treated as one feature. -// For example, the MTLView combines thses with one property, depthStencilPixelFormat. -// We can specify a pixel format like depth32Float_stencil8, -// as a way to say that we want both depth and stencil behavior. -// -// On the other hand, MTLRenderPassDescriptor has separate properties for -// depthAttachment vs stencilAttachment. -// Some of the config for these we want to be different, like the storeAction. -// But the actual textures backing these attachments can be the same object. -// -// Similarly, MTLRenderPipelineDescriptor has separate properties for -// depthAttachmentPixelFormat vs stencilAttachmentPixelFormat, -// even though these both might have the same value of depth32Float_stencil8. -// -// MTLDepthStencilDescriptor deals with both stencils and depth, -// this is where we specify behaviors like writing to stencil vs using a stencil as a mask. -// -// So, long way of saying -- the API is confusing and we need to coordinate -// things across a few different parts of the API. -// Instead of adding lots of conditionals, each time we touch one of those parts of the API, -// better to combine cohesive sets of behavior in one place here. -// // Created by Benjamin Heasly on 5/5/22. // Copyright © 2022 GRU. All rights reserved. // @@ -38,12 +9,105 @@ import Foundation import MetalKit -protocol mglDepthStencilConfig { +/* + mglDepthStencilState keeps track the current depth and stencil state for the app, including: + - whether we're creating or applying a stencil + - which of 8 stencil planes we're creating or applying, if any + + The Metal API combines depth config and stencil config into one slightly confusing concept. + So, this is also where we enable depth testing. + + Depth and stencil config needs to be applied at a couple of points for each render pass. + At each point we need to be consistent about what state we're in and which plane we're using. + mglDepthStencilState encloses the state-dependent consistency with a polymorphic/strategy approach, + which seems nicer than having lots of conditionals in the render pass setup code. + mglRenderer just needs to call configureRenderPassDescriptor() and configureRenderEncoder() at the right time. + */ +class mglDepthStencilState { + private let logger: mglLogger + + // For each stencil plane, a config we want to apply that stencil. + private var applyStencilConfig = [mglDepthStencilConfig]() + + // For each stencil plane, configs to use when we are creating that stencil. + private var createStencilConfig = [mglDepthStencilConfig]() + private var createInvertedStencilConfig = [mglDepthStencilConfig]() + + // The current config, one of the above. + private var currentDepthStencilConfig: mglDepthStencilConfig! + + init(logger: mglLogger, device: MTLDevice) { + self.logger = logger + + // Set up to support 8 stencil planes. + for index in 0 ..< 8 { + let number = UInt32(index) + + // Config to apply the stencil for this plane. + applyStencilConfig.append(mglEnableDepthAndStencilTest(stencilNumber: number, device: device)) + + // Configs to create the stencil for this plane. + createStencilConfig.append(mglEnableDepthAndStencilCreate(stencilNumber: number, isInverted: false, device: device)) + createInvertedStencilConfig.append(mglEnableDepthAndStencilCreate(stencilNumber: number, isInverted: true, device: device)) + } + + // By default, enable the 0th stencil plane (ie no stencil) along with depth testing. + currentDepthStencilConfig = applyStencilConfig[0] + } + + // Collaborate with mglRenderer to set up a render pass. + func configureRenderPassDescriptor(renderPassDescriptor: MTLRenderPassDescriptor) { + currentDepthStencilConfig.configureRenderPassDescriptor(renderPassDescriptor: renderPassDescriptor) + } + + // Collaborate with mglRenderer to set up a render pass. + func configureRenderEncoder(renderEncoder: MTLRenderCommandEncoder) { + currentDepthStencilConfig.configureRenderEncoder(renderEncoder: renderEncoder) + } + + // Collaborate with mglRenderer to start creating the stencil plane at the given number. + func startStencilCreation(view: MTKView, stencilNumber: UInt32, isInverted: Bool) -> Bool { + let stencilIndex = Array.Index(stencilNumber) + if (!createStencilConfig.indices.contains(stencilIndex)) { + logger.error(component: "mglDepthStencilState", details: "Got stencil number to create \(stencilNumber) but only numbers 0-7 are supported.") + return false + } + + logger.info(component: "mglDepthStencilState", details: "Creating stencil number \(stencilNumber), with isInverted \(isInverted).") + currentDepthStencilConfig = isInverted ? createInvertedStencilConfig[stencilIndex] : createStencilConfig[stencilIndex] + return true + } + + // Collaborate with mglRenderer to finish creating the stencil plane at the given number. + func finishStencilCreation(view: MTKView) -> Bool { + logger.info(component: "mglDepthStencilState", details: "Finishing stencil creation.") + currentDepthStencilConfig = applyStencilConfig[0] + return true + } + + // Collaborate with mglRenderer to apply the stencil plane at the given number. + func selectStencil(view: MTKView, renderEncoder: MTLRenderCommandEncoder, stencilNumber: UInt32) -> Bool { + let stencilIndex = Array.Index(stencilNumber) + if (!applyStencilConfig.indices.contains(stencilIndex)) { + logger.error(component: "mglDepthStencilState", details: "Got stencil number to select \(stencilNumber) but only numbers 0-7 are supported.") + return false + } + + logger.info(component: "mglDepthStencilState", details: "Selecting stencil number \(stencilNumber).") + currentDepthStencilConfig = applyStencilConfig[stencilIndex] + currentDepthStencilConfig.configureRenderEncoder(renderEncoder: renderEncoder) + return true + } +} + +// Abstract the depth and stencil operations needed for setting up a render pass. +private protocol mglDepthStencilConfig { func configureRenderPassDescriptor(renderPassDescriptor: MTLRenderPassDescriptor) func configureRenderEncoder(renderEncoder: MTLRenderCommandEncoder) } -class mglEnableDepthAndStencilTest : mglDepthStencilConfig { +// Implement details for how we apply a stencil plane and depth test. +private class mglEnableDepthAndStencilTest : mglDepthStencilConfig { let stencilMask: UInt32 let depthStencilState: MTLDepthStencilState? @@ -85,7 +149,8 @@ class mglEnableDepthAndStencilTest : mglDepthStencilConfig { } } -class mglEnableDepthAndStencilCreate : mglDepthStencilConfig { +// Implement details for how we create a stencil plane and depth test. +private class mglEnableDepthAndStencilCreate : mglDepthStencilConfig { let stencilMask: UInt32 let stencilRefValue: UInt32 let depthStencilState: MTLDepthStencilState? diff --git a/metal/mglMetal/mglLocalClient.swift b/metal/mglMetal/mglLocalClient.swift index 96e3a7b2..4028598b 100644 --- a/metal/mglMetal/mglLocalClient.swift +++ b/metal/mglMetal/mglLocalClient.swift @@ -7,17 +7,19 @@ // import Foundation -import os.log class mglLocalClient { + let logger: mglLogger let pollMilliseconds = Int32(10) let pathToConnect: String var socketDescriptor: Int32 - init(pathToConnect: String) { - os_log("(mglLocalClient) Starting with path to connect: %{public}@", log: .default, type: .info, String(describing: pathToConnect)) + init(logger: mglLogger, pathToConnect: String) { + self.logger = logger + + logger.info(component: "mglLocalClient", details: "Starting with path to connect: \(pathToConnect)") self.pathToConnect = pathToConnect socketDescriptor = socket(AF_UNIX, SOCK_STREAM, 0) @@ -45,7 +47,7 @@ class mglLocalClient { fatalError("(mglLocalClient) Could not connect to the path: \(connectResult) errno: \(errno)") } - os_log("(mglLocalClient) Ready and connected to path: %{public}@", log: .default, type: .info, String(describing: pathToConnect)) + logger.info(component: "mglLocalClient", details: "Ready and connected to path: \(pathToConnect)") } deinit { @@ -73,9 +75,9 @@ class mglLocalClient { func readData(buffer: UnsafeMutableRawPointer, expectedByteCount: Int) -> Int { let bytesRead = recv(socketDescriptor, buffer, expectedByteCount, MSG_WAITALL); if bytesRead < 0 { - os_log("(mglLocalClient) Error reading %{public}d bytes from server, read %{public}d, errno %{public}d", log: .default, type: .error, expectedByteCount, bytesRead, errno) + logger.error(component: "mglLocalClient", details: "Error reading \(expectedByteCount) bytes from server, read \(bytesRead), errno \(errno)") } else if bytesRead == 0 { - os_log("(mglLocalClient) Server disconnected before sending %{public}d bytes, disconnecting this end, too.", log: .default, type: .error, expectedByteCount) + logger.error(component: "mglLocalClient", details: "Server disconnected before sending \(expectedByteCount) bytes, disconnecting this end, too") disconnect() } return bytesRead @@ -95,9 +97,9 @@ class mglLocalClient { totalSent += sent } if totalSent < 0 { - os_log("(mglLocalClient) Error sending %{public}d bytes, sent %{public}d, errno %{public}d", log: .default, type: .error, byteCount, totalSent, errno) + logger.error(component: "mglLocalClient", details: "Error sending \(byteCount) bytes, sent \(totalSent), errno \(errno)") } else if totalSent != byteCount { - os_log("(mglLocalClient) Sent %{public}d bytes, but expected to send %{public}d, errno %{public}d", log: .default, type: .error, totalSent, byteCount, errno) + logger.error(component: "mglLocalClient", details: "Sent \(totalSent) bytes, but expected to send \(byteCount), errno \(errno)") } return totalSent } diff --git a/metal/mglMetal/mglLocalServer.swift b/metal/mglMetal/mglLocalServer.swift index ab5c40ca..7f3c5f0c 100644 --- a/metal/mglMetal/mglLocalServer.swift +++ b/metal/mglMetal/mglLocalServer.swift @@ -7,19 +7,23 @@ // import Foundation -import os.log class mglLocalServer : mglServer { + let logger: mglLogger let pathToBind: String - let maxConnections: Int32 = Int32(500) - let pollMilliseconds: Int32 = Int32(10) + let maxConnections: Int32 + let pollMilliseconds: Int32 let boundSocketDescriptor: Int32 var acceptedSocketDescriptor: Int32 = -1 - init(pathToBind: String, maxConnections: Int32 = Int32(500), pollMilliseconds: Int32 = Int32(10)) { - os_log("(mglLocalServer) Starting with path to bind: %{public}@", log: .default, type: .info, String(describing: pathToBind)) + init(logger: mglLogger, pathToBind: String, maxConnections: Int32 = Int32(500), pollMilliseconds: Int32 = Int32(10)) { + self.logger = logger + self.maxConnections = maxConnections + self.pollMilliseconds = pollMilliseconds + + logger.info(component: "mglLocalServer", details: "Starting with path to bind: \(pathToBind)") self.pathToBind = pathToBind if FileManager.default.fileExists(atPath: pathToBind) { @@ -66,7 +70,7 @@ class mglLocalServer : mglServer { fatalError("(mglLocalServer) Could not listen for connections: \(listenResult) errno: \(errno)") } - os_log("(mglLocalServer) Ready and listening for connections at path: %{public}@", log: .default, type: .info, String(describing: pathToBind)) + logger.info(component: "mglLocalServer", details: "Ready and listening for connections at path: \(pathToBind)") } deinit { @@ -94,24 +98,28 @@ class mglLocalServer : mglServer { acceptedSocketDescriptor = accept(boundSocketDescriptor, nil, nil) if (acceptedSocketDescriptor >= 0) { - os_log("(mglLocalServer) Accepted a new client connection at path: %{public}@", log: .default, type: .info, String(describing: pathToBind)) + logger.info(component: "mglLocalServer", details: "Accepted a new client connection at path: \(pathToBind)") return true } // Since this is a nonblockign socket, it's OK for accept to return -1 -- as long as errno is EAGAIN or EWOULDBLOCK. if (errno != EAGAIN && errno != EWOULDBLOCK) { - os_log("(mglLocalServer) Could not accept client connection, got result %{public}d, errno %{public}d", log: .default, type: .error, acceptedSocketDescriptor, errno) + logger.error(component: "mglLocalServer", details: "Could not accept client connection, got result \(acceptedSocketDescriptor), errno \(errno)") } return false; } func dataWaiting() -> Bool { + return dataWaiting(timeout: pollMilliseconds) + } + + func dataWaiting(timeout: Int32) -> Bool { var pfd = pollfd() pfd.fd = acceptedSocketDescriptor; pfd.events = Int16(POLLIN); pfd.revents = 0; _ = withUnsafeMutablePointer(to: &pfd) { - poll($0, 1, pollMilliseconds) + poll($0, 1, timeout) } return pfd.revents == POLLIN } @@ -135,9 +143,9 @@ class mglLocalServer : mglServer { } if totalRead < 0 { - os_log("(mglLocalServer) Error reading %{public}d bytes from server, read %{public}d, errno %{public}d", log: .default, type: .error, expectedByteCount, totalRead, errno) + logger.error(component: "mglLocalServer", details: "Error reading \(expectedByteCount) bytes from server, read \(totalRead), errno \(errno)") } else if totalRead == 0 { - os_log("(mglLocalServer) Client disconnected before sending %{public}d bytes, disconnecting this end, too.", log: .default, type: .error, expectedByteCount) + logger.error(component: "mglLocalServer", details: "Client disconnected before sending \(expectedByteCount) bytes, disconnecting this end, too.") disconnect() } return totalRead @@ -157,12 +165,10 @@ class mglLocalServer : mglServer { totalSent += sent } if totalSent < 0 { - os_log("(mglLocalServer) Error sending %{public}d bytes, sent %{public}d, errno %{public}d", log: .default, type: .error, byteCount, totalSent, errno) + logger.error(component: "mglLocalServer", details: "Error sending \(byteCount) bytes, sent \(totalSent), errno \(errno)") } else if totalSent != byteCount { - os_log("(mglLocalServer) Sent %{public}d bytes, but expected to send %{public}d, errno %{public}d", log: .default, type: .error, totalSent, byteCount, errno) - + logger.error(component: "mglLocalServer", details: "Sent \(totalSent) bytes, but expected to send \(byteCount), errno \(errno)") } return totalSent } - } diff --git a/metal/mglMetal/mglLogger.swift b/metal/mglMetal/mglLogger.swift new file mode 100644 index 00000000..d601fdaa --- /dev/null +++ b/metal/mglMetal/mglLogger.swift @@ -0,0 +1,64 @@ +// +// mglLogger.swift +// mglMetal +// +// Created by Benjamin Heasly on 1/4/24. +// Copyright © 2024 GRU. All rights reserved. +// + +import Foundation +import OSLog + +// A logger gives other app components a convenient interface for logging with. +// It also remembers the last error message, so we can report this back to the client. +protocol mglLogger { + func info(component: String, details: String) + func error(component: String, details: String) + func getErrorMessage() -> String +} + +// Apple is migrating logging apis, so check version and return the preferred implementation. +// https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code +func getMglLogger() -> mglLogger { + if #available(macOS 11.0, *) { + return mglMacOs11PLusLogger() + } else { + return mglLegacyLogger() + } +} + +@available(macOS 11.0, *) +private class mglMacOs11PLusLogger : mglLogger { + private let logger = Logger() + private var lastErrorMessage: String = "(mglMacOs11PLusLogger) No errors logged yet." + + func info(component: String, details: String) { + logger.info("(\(component, privacy: .public)) \(details, privacy: .public)") + } + + func error(component: String, details: String) { + lastErrorMessage = "(\(component)) \(details)" + logger.error("(\(component, privacy: .public)) \(details, privacy: .public)") + } + + func getErrorMessage() -> String { + return lastErrorMessage + } +} + +private class mglLegacyLogger : mglLogger { + private var lastErrorMessage: String = "(mglLegacyLogger) No errors logged yet." + + func info(component: String, details: String) { + os_log("(%{public}@) %{public}@", log: .default, type: .info, component, details) + } + + func error(component: String, details: String) { + lastErrorMessage = "(\(component)) \(details)" + os_log("(%{public}@) %{public}@", log: .default, type: .error, component, details) + } + + func getErrorMessage() -> String { + return lastErrorMessage + } +} diff --git a/metal/mglMetal/mglRenderer.swift b/metal/mglMetal/mglRenderer.swift deleted file mode 100644 index ae25900f..00000000 --- a/metal/mglMetal/mglRenderer.swift +++ /dev/null @@ -1,1501 +0,0 @@ -//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ -// -// mglRenderer.swift -// mglMetal -// -// Created by justin gardner on 12/28/2019. -// Copyright © 2019 GRU. All rights reserved. -// -//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - -//\/\/\/\/\/\/\/\/\/\/\/\/\/\/ -// Include section -//\/\/\/\/\/\/\/\/\/\/\/\/\/\/ -import Foundation -import MetalKit -import AppKit -import os.log -import GameplayKit - - -//\/\/\/\/\/\/\/\/\/\/\/\/\/\/ -// mglRenderer: Class does most of the work -// handles initializing of the GPU, pipeline states etc -// handles the frame updates and drawing as well as resizing -//\/\/\/\/\/\/\/\/\/\/\/\/\/\/ -class mglRenderer: NSObject { - // GPU Device - static var device : MTLDevice! - - // commandQueue which tells the device what to do - static var commandQueue: MTLCommandQueue! - - // library holds compiled vertex and fragment shader programs - let library: MTLLibrary! - - // configuration for doing depth and stencil testing - var enableDepthAndStencilConfig = [mglDepthStencilConfig]() - var createStencilConfig = [mglDepthStencilConfig]() - var createInvertedStencilConfig = [mglDepthStencilConfig]() - var currentDepthStencilConfig: mglDepthStencilConfig - - // command interface communicates with the client process like Matlab - let commandInterface : mglCommandInterface - - // a flag used to send post-flush acknowledgements back to Matlab - var acknowledgeFlush = false - - // State to manage commands that repeat themselves over multiple frames/render passes. - // These drive several different conditionals below that are coupled and need to work in concert. - // If/when we develop an explicit OOP model for commands, - // It might be good to refactor these areas using polymorphism, something like the strategy pattern. - // For example, we might want to have just a currentCommand var here, - // And then we could move any other state into implementations of mglCommandModel (which is currently just an idea). - var repeatingCommandFrameCount: UInt32 = 0 - var repeatingCommandCode: mglCommandCode = mglUnknownCommand - var repeatingCommandRandomSouce: GKRandomSource = GKMersenneTwisterRandomSource(seed: 0) - var repeatingCommandObjectCount: UInt32 = 0 - - // keeps coordinate xform - var deg2metal = matrix_identity_float4x4 - - // a collection of user-managed textures to render to and/or blt to screen - var textureSequence = UInt32(1) - var textures : [UInt32: MTLTexture] = [:] - - // pipeline and other configuration to render to the screen or a texture - var onscreenRenderingConfig: mglColorRenderingConfig - var currentColorRenderingConfig: mglColorRenderingConfig - - // utility to get system nano time - let secs = mglSecs() - - // a string to store the last error message (which can be retrieved via - // the command mglGetErrorMessage - var errorMessage = "" - - // flag used in render looop to set whether command has succeeded or not - var commandSuccess = false - - // flag to keep track of cursor stye - var cursorHidden = false - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // init - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - init(metalView: MTKView) { - // bind an address and start listening for client process to connect - commandInterface = mglCommandInterface() - - // Initialize the GPU device - guard let device = MTLCreateSystemDefaultDevice() else { - fatalError("GPU not available") - } - metalView.device = device - mglRenderer.device = device - - // initialize the command queue - mglRenderer.commandQueue = device.makeCommandQueue()! - - // create a library for storing the shaders - library = device.makeDefaultLibrary() - - // Set up to enable depth testing and stenciling. - // Confusingly, some parts of the Metal API treat these as one feature, - // while other parts treat depth and stenciling as separate features. - // In this case, the view treates them as one, with one big pixel format (and one buffer/texture) that handles both. - metalView.depthStencilPixelFormat = .depth32Float_stencil8 - metalView.clearDepth = 1.0 - - // Default to depth test enabled, but no stenciling enabled. - // This currentDepthStencilConfig can be swapped out later to create and select stencils by number. - for index in 0 ..< 8 { - enableDepthAndStencilConfig.append(mglEnableDepthAndStencilTest(stencilNumber: UInt32(index), device: device)) - createStencilConfig.append(mglEnableDepthAndStencilCreate(stencilNumber: UInt32(index), isInverted: false, device: device)) - createInvertedStencilConfig.append(mglEnableDepthAndStencilCreate(stencilNumber: UInt32(index), isInverted: true, device: device)) - } - currentDepthStencilConfig = enableDepthAndStencilConfig[0] - - // Start with default clear color gray, used for on-screen as well as render to texture. - metalView.clearColor = MTLClearColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1.0) - - // Default to onscreen rendering config. - guard let onscreenRenderingConfig = mglOnscreenRenderingConfig(device: device, library: library, view: metalView) else { - fatalError("Could not create onscreen rendering config, got nil!") - } - self.onscreenRenderingConfig = onscreenRenderingConfig - self.currentColorRenderingConfig = onscreenRenderingConfig - - // init the super class - super.init() - - // Tell the view that this class will be used as the - // delegate - this makes it so that the view will call - // the draw function each frame update and the resize function - metalView.delegate = self - - os_log("(mglRenderer) Init OK.", log: .default, type: .info) - } -} - -extension mglRenderer: MTKViewDelegate { - func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { - os_log("(mglRenderer) drawableSizeWillChange %{public}@", log: .default, type: .info, String(describing: size)) - } - - // We expect the view to be configured for "Timed updates" (the default), - // which is the traditional way of running once per "video frame", "screen refresh", etc. - // as described here: - // https://developer.apple.com/documentation/metalkit/mtkview?language=objc - func draw(in view: MTKView) { - // Using autoreleasepool lets us attempt to release system resources like the "drawable" as soon as we're done. - // Apple's guidance is to do this once per frame, rather than letting it happen lazily at some later time. - // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html - autoreleasepool { - render(in: view) - } - } - - // This is the main "loop" for mglMetal. - // We're currently doing all our app updates here, not just drawing. - // This includes tasks like: - // - accepting pending socket connections from Matlab - // - reading comand codes and data from Matlab - // - writing command acks and results back to Matlab - // - executing non-drawing commands like texture data management, window management, etc. - // - executing actual drawing commands! - private func render(in view: MTKView) { - // Check if a client connection is already accepted, or try to accept a new one. - let clientIsConnected = commandInterface.acceptClientConnection() - if !clientIsConnected { - // Nothing to do if client isn't connected. We'll try again on the next draw. - return - } - - // Write a post-flush timestamp, which is the command-processed ack for the previous draw. - if acknowledgeFlush { - acknowledgePreviousCommandProcessed(isSuccess: true, whichCommand: mglFlush) - acknowledgeFlush = false - } - - // Do we have a command to process? - var command: mglCommandCode - if repeatingCommandFrameCount > 0 { - // Yes, were in a repeated command sequence. - command = repeatingCommandCode - } else if commandInterface.dataWaiting() { - // Yes, we have a new command from Matlab. - command = readAndAcknowledgeNextCommand() - } else { - // No, we'll check again on the next frame / draw() call. - return; - } - - // Process non-drawing commands one at a time. - // This avoids holding expensive drawing resources until we need to (below) as described here: - // https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc#3385893 - if command.rawValue < mglDrawingCommands.rawValue { - commandSuccess = false - - switch command { - case mglPing: commandSuccess = commandInterface.writeCommand(data: mglPing) == mglSizeOfCommandCodeArray(1) - case mglDrainSystemEvents: commandSuccess = drainSystemEvents(view: view) - case mglFullscreen: commandSuccess = fullscreen(view: view) - case mglWindowed: commandSuccess = windowed(view: view) - case mglCreateTexture: commandSuccess = createTexture() - case mglReadTexture: commandSuccess = readTexture() - case mglSetRenderTarget: commandSuccess = setRenderTarget(view: view) - case mglSetWindowFrameInDisplay: commandSuccess = setWindowFrameInDisplay(view: view) - case mglGetWindowFrameInDisplay: commandSuccess = getWindowFrameInDisplay(view: view) - case mglDeleteTexture: commandSuccess = deleteTexture() - case mglSetViewColorPixelFormat: commandSuccess = setViewColorPixelFormat(view: view) - case mglStartStencilCreation: commandSuccess = startStencilCreation(view: view) - case mglFinishStencilCreation: commandSuccess = finishStencilCreation(view: view) - case mglInfo: commandSuccess = sendAppInfo(view: view) - case mglGetErrorMessage: commandSuccess = getErrorMessage(view: view) - case mglFrameGrab: commandSuccess = frameGrab(view: view) - case mglMinimize: commandSuccess = minimize(view: view) - case mglDisplayCursor: commandSuccess = displayCursor(view: view) - default: - errorMessage = "(mglRenderer) Unknown non-drawing command code \(String(describing: command))" - os_log("(mglRenderer) Unknown non-drawing command code %{public}@", log: .default, type: .error, String(describing: command)) - } - - acknowledgePreviousCommandProcessed(isSuccess: commandSuccess, whichCommand: command) - return - } - - // From here below, we will process the next command as a drawing command. - // This means setting up a Metal rendering pass and continuing to process commands until an mglFlush command. - // Or, if there's an error, we'll abandon the rendering pass and return a negative ack. - - // Clear color is a unique case. - // We need to process it before setting up the rendering pass, - // so that the frame buffer texture can be cleared to the correct color when it gets loaded at the start of the rendering pass. - // We don't want to return immediately, as we do with non-drawing commands above, because that incurs a frame wait. - // Instead we want to fall into the rendering tight loop below. - // But we don't want to process the clear command along with other drawing commands because by then it's too late - // the texture will have been loaded and cleared already, using the old clear color. - // So we process it here in the middle, a special case here. - var colorHasBeenSet = false - if command == mglSetClearColor { - // run setClearColor - commandSuccess = setClearColor(view: view) - // keep that the color has been set, since we need - // that below when we finish the processing of this command - colorHasBeenSet = true - if (!commandSuccess) { - // acknwoledge processing only if this was an error, otherwise - // wait till we get the drawable before sending sucess - acknowledgePreviousCommandProcessed(isSuccess: false, whichCommand: command) - errorMessage = "(mglRenderer) Error setting clear color, skipping render pass." - os_log("(mglRenderer) Error setting clear color, skipping render pass.", log: .default, type: .error) - return - } - } - - // This call to view.currentDrawable accesses an expensive system resource, the "drawable". - // Normally this is fast and not a problem. - // But we've seen it get slow when using large amounts of video memory. - // For example when we have 30 full-screen-sized textures loaded at once. - // For rgba Float32 textures, 30 of these can take up about a GB of memory. - // In this case the call duration is sometimes <1ms, sometimes ~7ms, sometimes even ~14 ms. - // It's not entirely consistent, but the durations seem to come in runs that last for many frames or several seconds. - // Even when in a run of ~14ms durations, we don't necessarily drop more frames than usual. - // When we do drop a frame, this seems to kick off a new run with a different call duration, for a while. - // We may be seeing some blocking/synchronizing on the expensive "drawable" resource. - // We seem to be already following the guidance about the "drawable" as discussed in Apple docs: - // https://developer.apple.com/documentation/metalkit/mtkview - // https://developer.apple.com/documentation/quartzcore/cametallayer#3385893 - // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html - // In particular, we're doing our rendering work inside an autoreleasepool {} block, - // and we're not acquiring the drawable until we really are about to render a frame. - // So, are we just finding out how to tax the system and seeing what happens in that case? - // Does the system "know best" and we are blocking/synchronizing as expected? - // Or is there somethign else we can do about these long call durations? - guard let drawable = view.currentDrawable else { - errorMessage = "(mglRenderer) Could not get current drawable, aborting render pass." - os_log("(mglRenderer) Could not get current drawable, aborting render pass.", log: .default, type: .error) - acknowledgePreviousCommandProcessed(isSuccess: false, whichCommand: command) - return - } - - // This call to getRenderPassDescriptor(view: view) internally calls view.currentRenderPassDescriptor. - // The call to view.currentRenderPassDescriptor impicitly accessed the view's currentDrawable, as mentioned above. - // It's possible to swap the order of these calls. - // But whichever one we call first seems to pay the same blocking/synchronization price when memory usage is high. - guard let renderPassDescriptor = currentColorRenderingConfig.getRenderPassDescriptor(view: view) else { - errorMessage = "(mglRenderer) Could not get render pass descriptor from current color rendering config, aborting render pass." - os_log("(mglRenderer) Could not get render pass descriptor from current color rendering config, aborting render pass.", log: .default, type: .error) - // we have failed, so return failure to acknwoledge previous command - acknowledgePreviousCommandProcessed(isSuccess: false, whichCommand: command) - return - } - currentDepthStencilConfig.configureRenderPassDescriptor(renderPassDescriptor: renderPassDescriptor) - - guard let commandBuffer = mglRenderer.commandQueue.makeCommandBuffer(), - let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { - errorMessage = "(mglRenderer) Could not get command buffer and renderEncoder from the command queue, aborting render pass." - os_log("(mglRenderer) Could not get command buffer and renderEncoder from the command queue, aborting render pass.", log: .default, type: .error) - acknowledgePreviousCommandProcessed(isSuccess: false, whichCommand: command) - return - } - currentDepthStencilConfig.configureRenderEncoder(renderEncoder: renderEncoder) - - // Attach our view transform to the same location expected by all vertex shaders (our convention). - renderEncoder.setVertexBytes(°2metal, length: MemoryLayout.stride, index: 1) - - // Keep processing drawing and other related commands until a flush command. - while (command != mglFlush) { - // Clear color is processed as a special case above. - // All we need to do now is wait for other drawing commands, or a flush command. - if command == mglSetClearColor { - // if this has been called in the middle of the tight loop, - // (rather then at the beginning), we will need to read - // the color and set it, although it won't have any effect - // until the next frame - if !colorHasBeenSet { - // run setClearColor - commandSuccess = setClearColor(view: view) - } - // now acknowledge processing - acknowledgePreviousCommandProcessed(isSuccess: commandSuccess, whichCommand: command) - - // read the next command - command = readAndAcknowledgeNextCommand() - // and set that colorHasBeenSet to false (since this - // is a new command - and if it happens to be a mglClearScreen, - // then it's color will not have been set. - colorHasBeenSet = false - continue - } - - // Proces the next drawing command within the current render pass. - commandSuccess = false - switch command { - case mglCreateTexture: commandSuccess = createTexture() - case mglBltTexture: commandSuccess = bltTexture(view: view, renderEncoder: renderEncoder) - case mglDeleteTexture: commandSuccess = deleteTexture() - case mglSetXform: commandSuccess = setXform(renderEncoder: renderEncoder) - case mglDots: commandSuccess = drawDots(view: view, renderEncoder: renderEncoder) - case mglLine: commandSuccess = drawVerticesWithColor(view: view, renderEncoder: renderEncoder, primitiveType: .line) - case mglQuad: commandSuccess = drawVerticesWithColor(view: view, renderEncoder: renderEncoder, primitiveType: .triangle) - case mglPolygon: commandSuccess = drawVerticesWithColor(view: view, renderEncoder: renderEncoder, primitiveType: .triangleStrip) - case mglArcs: commandSuccess = drawArcs(view: view, renderEncoder: renderEncoder) - case mglUpdateTexture: commandSuccess = updateTexture() - case mglSelectStencil: commandSuccess = selectStencil(view: view, renderEncoder: renderEncoder) - case mglRepeatFlicker: commandSuccess = repeatFlicker(view: view, renderEncoder: renderEncoder) - case mglRepeatBlts: commandSuccess = repeatBlts(view: view, renderEncoder: renderEncoder) - case mglRepeatQuads: commandSuccess = repeatQuads(view: view, renderEncoder: renderEncoder) - case mglRepeatDots: commandSuccess = repeatDots(view: view, renderEncoder: renderEncoder) - case mglRepeatFlush: commandSuccess = repeatFlush(view: view, renderEncoder: renderEncoder) - default: - errorMessage = "(mglRenderer) Unknown drawing command code \(String(describing: command))" - os_log("(mglRenderer) Unknown drawing command code %{public}@", log: .default, type: .error, String(describing: command)) - } - - if !commandSuccess { - errorMessage = "(mglRenderer) Error processing drawing command \(String(describing: command)), aborting render pass." - os_log("(mglRenderer) Error processing drawing command %{public}@, aborting render pass", log: .default, type: .error, String(describing: command)) - acknowledgePreviousCommandProcessed(isSuccess: false, whichCommand: command) - renderEncoder.endEncoding() - return - } - - // We're in a repeating command sequence, so flush to the next frame automatically. - if repeatingCommandFrameCount > 0 { - acknowledgeRepeatingCommandAutomaticFlush() - repeatingCommandFrameCount -= 1 - break - } - - // Acknowledge this command was processed OK. - acknowledgePreviousCommandProcessed(isSuccess: true, whichCommand: command) - - // This will block until the next command arrives. - // The idea is to process a sequence of drawing commands as fast as we can within a frame. - command = readAndAcknowledgeNextCommand() - } - - // If we got here, we just did some drawing, ending with a flush command. - // We'll wait until the next frame starts before acknowledging that the render pass was fully processed. - acknowledgeFlush = true - - // Present the drawable, and do other things like synchronize texture buffers, if needed. - renderEncoder.endEncoding() - - currentColorRenderingConfig.finishDrawing(commandBuffer: commandBuffer, drawable: drawable) - } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // readAndAcknowledgePreviousCommand - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - private func readAndAcknowledgeNextCommand() -> mglCommandCode { - guard let command = commandInterface.readCommand() else { - _ = commandInterface.writeDouble(data: -secs.get()) - return mglUnknownCommand - } - _ = commandInterface.writeDouble(data: secs.get()) - return command - } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // acknowledgeRepeatingCommandAutomaticFlush - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - private func acknowledgeRepeatingCommandAutomaticFlush() { - _ = commandInterface.writeDouble(data: secs.get()) - } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // acknowledgePreviousCommandProcessed - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - private func acknowledgePreviousCommandProcessed(isSuccess: Bool, whichCommand: mglCommandCode = mglUnknownCommand) { - if isSuccess { - // success send back the time - _ = commandInterface.writeDouble(data: secs.get()) - } else { - // log which command failed - os_log("(mglRenderer) %{public}@ failed", log: .default, type: .error, String(describing: whichCommand)) - // clear any data that a command might have sent - commandInterface.clearReadData() - // failure is signaled by negative time. - _ = commandInterface.writeDouble(data: -secs.get()) - } - } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // acknowledgeReturnDataOnItsWay - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - private func acknowledgeReturnDataOnItsWay(isOnItsWay: Bool) { - if isOnItsWay { - _ = commandInterface.writeDouble(data: secs.get()) - } else { - _ = commandInterface.writeDouble(data: -secs.get()) - } - } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // Non-drawing commands - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // displayCursor - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - private func displayCursor(view: MTKView) -> Bool { - // Get whether this is a minimize (0) or restore (1) - guard let displayOrHide = commandInterface.readUInt32() else { - return false - } - - if displayOrHide == 0 { - // Hide the cursor - if !self.cursorHidden { - NSCursor.hide() - self.cursorHidden = true - } - } - else { - // Show the cursor - if self.cursorHidden { - NSCursor.unhide() - self.cursorHidden = false - } - - } - return true - } - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // minimize - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - private func minimize(view: MTKView) -> Bool { - // Get whether this is a minimize (0) or restore (1) - guard let minimizeOrRestore = commandInterface.readUInt32() else { - return false - } - - if minimizeOrRestore == 0 { - // minimize - view.window?.miniaturize(nil) - } - else { - // restore - view.window?.deminiaturize(nil) - - } - return true - } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // frameGrab - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - private func frameGrab(view: MTKView) -> Bool { - // grab from from the currentColorRenderingTarget. Note that - // this will return (0,0,nil) if the current target is the screen - // as it is not implemented (and might be hard/impossible?) to get - // the bytes from that. So, this only works if the current target - // is a texture (which is set by mglMetalSetRenderTarget - let frame = currentColorRenderingConfig.frameGrab() - // write out the width and height - _ = commandInterface.writeUInt32(data: UInt32(frame.width)) - _ = commandInterface.writeUInt32(data: UInt32(frame.height)) - if frame.pointer != nil { - // convert the pointer back into an array - let floatArray = Array(UnsafeBufferPointer(start: frame.pointer, count: frame.width*frame.height*4)) - // write the array - _ = commandInterface.writeFloatArray(data: floatArray) - // free the data - frame.pointer?.deallocate() - return true - } - else { - return false - } - } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // getErrorMessage - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - private func getErrorMessage(view: MTKView) -> Bool { - // send error message - _ = commandInterface.writeString(data: errorMessage) - return true - } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // sendAppInfo - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - private func sendAppInfo(view: MTKView) -> Bool { - // send GPU name - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "gpu.name") - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: mglRenderer.device.name) - - // send GPU registryID - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "gpu.registryID") - _ = commandInterface.writeCommand(data: mglSendDouble) - _ = commandInterface.writeDouble(data: Double(mglRenderer.device.registryID)) - - // send currentAllocatedSize - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "gpu.currentAllocatedSize") - _ = commandInterface.writeCommand(data: mglSendDouble) - _ = commandInterface.writeDouble(data: Double(mglRenderer.device.currentAllocatedSize)) - - // send recommendedMaxWorkingSetSize - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "gpu.recommendedMaxWorkingSetSize") - _ = commandInterface.writeCommand(data: mglSendDouble) - _ = commandInterface.writeDouble(data: Double(mglRenderer.device.recommendedMaxWorkingSetSize)) - - // send hasUnifiedMemory - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "gpu.hasUnifiedMemory") - _ = commandInterface.writeCommand(data: mglSendDouble) - _ = commandInterface.writeDouble(data: mglRenderer.device.hasUnifiedMemory ? 1.0 : 0.0) - - // send maxTransferRate - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "gpu.maxTransferRate") - _ = commandInterface.writeCommand(data: mglSendDouble) - _ = commandInterface.writeDouble(data: Double(mglRenderer.device.maxTransferRate)) - - // send minimumLinearTextureAlignment - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "gpu.minimumTextureBufferAlignment") - _ = commandInterface.writeCommand(data: mglSendDouble) - _ = commandInterface.writeDouble(data: Double(mglRenderer.device.minimumTextureBufferAlignment(for: .rgba32Float))) - - // send minimumLinearTextureAlignment - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "gpu.minimumLinearTextureAlignment") - _ = commandInterface.writeCommand(data: mglSendDouble) - _ = commandInterface.writeDouble(data: Double(mglRenderer.device.minimumLinearTextureAlignment(for: .rgba32Float))) - - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "view.colorPixelFormat") - _ = commandInterface.writeCommand(data: mglSendDouble) - _ = commandInterface.writeDouble(data: Double(view.colorPixelFormat.rawValue)) - - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "view.colorPixelFormatString") - _ = commandInterface.writeCommand(data: mglSendString) - switch view.colorPixelFormat { - case MTLPixelFormat.bgra8Unorm: _ = commandInterface.writeString(data: "bgra8Unorm") - case MTLPixelFormat.bgra8Unorm_srgb: _ = commandInterface.writeString(data: "bgra8Unorm_srgb") - case MTLPixelFormat.rgba16Float: _ = commandInterface.writeString(data: "rgba16Float") - case MTLPixelFormat.rgb10a2Unorm: _ = commandInterface.writeString(data: "rgb10a2Unorm") - case MTLPixelFormat.bgr10a2Unorm: _ = commandInterface.writeString(data: "bgr10a2Unorm") - default: _ = commandInterface.writeString(data: "Unknown") - } - - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "view.clearColor") - _ = commandInterface.writeCommand(data: mglSendDoubleArray) - let colorArray: [Double] = [Double(view.clearColor.red),Double(view.clearColor.green),Double(view.clearColor.blue),Double(view.clearColor.alpha)] - _ = commandInterface.writeDoubleArray(data: colorArray) - - _ = commandInterface.writeCommand(data: mglSendString) - _ = commandInterface.writeString(data: "view.drawableSize") - _ = commandInterface.writeCommand(data: mglSendDoubleArray) - let drawableSize: [Double] = [Double(view.drawableSize.width), Double(view.drawableSize.height)] - _ = commandInterface.writeDoubleArray(data: drawableSize) - - // send finished - _ = commandInterface.writeCommand(data: mglSendFinished) - - return true - } - - func setViewColorPixelFormat(view: MTKView) -> Bool { - guard let formatIndex = commandInterface.readUInt32() else { - return false - } - - switch formatIndex { - case 0: view.colorPixelFormat = .bgra8Unorm - case 1: view.colorPixelFormat = .bgra8Unorm_srgb - case 2: view.colorPixelFormat = .rgba16Float - case 3: view.colorPixelFormat = .rgb10a2Unorm - case 4: view.colorPixelFormat = .bgr10a2Unorm - default: view.colorPixelFormat = .bgra8Unorm - } - - // Recreate the onscreen color rendering config so that render pipelines will use the new color pixel format. - guard let newOnscreenRenderingConfig = mglOnscreenRenderingConfig(device: mglRenderer.device, library: library, view: view) else { - os_log("(mglRenderer) Could not create onscreen rendering config for pixel format %{public}@.", log: .default, type: .error, String(describing: view.colorPixelFormat)) - return false - } - - if (self.currentColorRenderingConfig is mglOnscreenRenderingConfig) { - // Start using the new config right away! - self.currentColorRenderingConfig = newOnscreenRenderingConfig - } - - // Remember the new onscreen config for later, even if we're currently rendering offscreen. - self.onscreenRenderingConfig = newOnscreenRenderingConfig - - return true - } - - func drainSystemEvents(view: MTKView) -> Bool { - guard let window = view.window else { - os_log("(mglRenderer) Could not get window from view, skipping drain events command.", log: .default, type: .error) - return false - } - - var event = window.nextEvent(matching: .any) - while (event != nil) { - //os_log("(mglRenderer) Processing OS event: %{public}@", log: .default, type: .info, String(describing: event)) - event = window.nextEvent(matching: .any) - } - - return true - } - - func windowed(view: MTKView) -> Bool { - NSCursor.unhide() - - guard let window = view.window else { - os_log("(mglRenderer) Could not get window from view, skipping windowed command.", log: .default, type: .error) - return false - } - - if !window.styleMask.contains(.fullScreen) { - os_log("(mglRenderer) App is already windowed, skipping windowed command.", log: .default, type: .info) - } else { - window.toggleFullScreen(nil) - } - - return true - } - - func setWindowFrameInDisplay(view: MTKView) -> Bool { - // Read all inputs first. - guard let displayNumber = commandInterface.readUInt32(), - let windowX = commandInterface.readUInt32(), - let windowY = commandInterface.readUInt32(), - let windowWidth = commandInterface.readUInt32(), - let windowHeight = commandInterface.readUInt32() else { - return false - } - - // Convert Matlab's 1-based display number to a zero-based screen index. - let screenIndex = displayNumber == 0 ? Array.Index(0) : Array.Index(displayNumber - 1) - - // Location of the chosen display AKA screen, according to the system desktop manager. - // Units might be hi-res "points", convert to native display pixels AKA "backing" as needed. - let screens = NSScreen.screens - let screen = screens.indices.contains(screenIndex) ? screens[screenIndex] : screens[0] - let screenNativeFrame = screen.convertRectToBacking(screen.frame) - - // Location of the window relative to the chosen display, in native pixels - let x = Int(screenNativeFrame.origin.x) + Int(windowX) - let y = Int(screenNativeFrame.origin.y) + Int(windowY) - let windowNativeFrame = NSRect(x: x, y: y, width: Int(windowWidth), height: Int(windowHeight)) - - // Location of the window in hi-res "points", or whatever, depending on system config. - let windowScreenFrame = screen.convertRectFromBacking(windowNativeFrame) - - guard let window = view.window else { - os_log("(mglRenderer) Could not get window from view, skipping set window frame command.", log: .default, type: .error) - return false - } - - if window.styleMask.contains(.fullScreen) { - os_log("(mglRenderer) App is fullscreen, skipping set window frame command.", log: .default, type: .info) - return false - } - - os_log("(mglRenderer) Setting window to display %{public}d frame %{public}@.", log: .default, type: .info, displayNumber, String(describing: windowScreenFrame)) - window.setFrame(windowScreenFrame, display: true) - - return true - } - - func getWindowFrameInDisplay(view: MTKView) -> Bool { - guard let window = view.window else { - os_log("(mglRenderer) Could get window from view, skipping get window frame command.", log: .default, type: .error) - acknowledgeReturnDataOnItsWay(isOnItsWay: false) - return false - } - - guard let screen = window.screen else { - os_log("(mglRenderer) Could get screen from window, skipping get window frame command.", log: .default, type: .error) - acknowledgeReturnDataOnItsWay(isOnItsWay: false) - return false - } - - guard let screenIndex = NSScreen.screens.firstIndex(of: screen) else { - os_log("(mglRenderer) Could get screen index from screens, skipping get window frame command.", log: .default, type: .error) - acknowledgeReturnDataOnItsWay(isOnItsWay: false) - return false - } - - // Convert 0-based screen index to Matlab's 1-based display number. - let displayNumber = mglUInt32(screenIndex + 1) - - // Return the position of the window relative to its screen, in pixel units not hi-res "points". - let windowNativeFrame = screen.convertRectToBacking(window.frame) - let screenNativeFrame = screen.convertRectToBacking(screen.frame) - let windowX = windowNativeFrame.origin.x - screenNativeFrame.origin.x - let windowY = windowNativeFrame.origin.y - screenNativeFrame.origin.y - acknowledgeReturnDataOnItsWay(isOnItsWay: true) - _ = commandInterface.writeUInt32(data: displayNumber) - _ = commandInterface.writeUInt32(data: mglUInt32(windowX)) - _ = commandInterface.writeUInt32(data: mglUInt32(windowY)) - _ = commandInterface.writeUInt32(data: mglUInt32(windowNativeFrame.width)) - _ = commandInterface.writeUInt32(data: mglUInt32(windowNativeFrame.height)) - return true - } - - func fullscreen(view: MTKView) -> Bool { - guard let window = view.window else { - os_log("(mglRenderer) Could not get window from view, skipping fullscreen command.", log: .default, type: .error) - return false - } - - if window.styleMask.contains(.fullScreen) { - os_log("(mglRenderer) App is already fullscreen, skipping fullscreen command.", log: .default, type: .info) - } else { - window.toggleFullScreen(nil) - NSCursor.hide() - } - - return true - } - - func setClearColor(view: MTKView) -> Bool { - guard let color = commandInterface.readColor() else { - return false - } - - view.clearColor = MTLClearColor(red: Double(color[0]), green: Double(color[1]), blue: Double(color[2]), alpha: 1) - return true - } - - func createTexture() -> Bool { - guard let texture = commandInterface.createTexture(device: mglRenderer.device) else { - return false - } - - // Consume a texture number from the bookkeeping sequence. - let newTextureNumber = textureSequence - textureSequence += 1 - textures[newTextureNumber] = texture - - // Return the new texture's number and the total count of textures. - acknowledgeReturnDataOnItsWay(isOnItsWay: true) - _ = commandInterface.writeUInt32(data: newTextureNumber) - _ = commandInterface.writeUInt32(data: mglUInt32(textures.count)) - return true - } - - func deleteTexture() -> Bool { - guard let textureNumber = commandInterface.readUInt32() else { - return false - } - - let removed = textures.removeValue(forKey: textureNumber) - if removed == nil { - os_log("(mglRenderer) Invalid texture number %{public}d, valid numbers are %{public}@.", log: .default, type: .error, textureNumber, String(describing: textures.keys)) - return false - } - - os_log("(mglRenderer) Removed texture number %{public}d, remaining numbers are %{public}@.", log: .default, type: .info, textureNumber, String(describing: textures.keys)) - return true - } - - func setRenderTarget(view: MTKView) -> Bool { - guard let textureNumber = commandInterface.readUInt32() else { - return false - } - - guard let targetTexture = textures[textureNumber] else { - os_log("(mglRenderer) Got textureNumber %{public}d, choosing onscreen rendering.", log: .default, type: .info, textureNumber) - currentColorRenderingConfig = onscreenRenderingConfig - return true - } - - os_log("(mglRenderer) Got textureNumber %{public}d, choosing offscreen rendering to texture.", log: .default, type: .info, textureNumber) - - guard let newTextureRenderingConfig = mglOffScreenTextureRenderingConfig(device: mglRenderer.device, library: library, view: view, texture: targetTexture) else { - os_log("(mglRenderer) Could not create offscreen rendering config for textureNumber %{public}d, got nil.", log: .default, type: .error, textureNumber) - return false - } - currentColorRenderingConfig = newTextureRenderingConfig - - return true - } - - func readTexture() -> Bool { - guard let textureNumber = commandInterface.readUInt32() else { - acknowledgeReturnDataOnItsWay(isOnItsWay: false) - return false - } - - guard let texture = textures[textureNumber] else { - os_log("(mglRenderer) Invalid texture number %{public}d, valid numbers are %{public}@.", log: .default, type: .error, textureNumber, String(describing: textures.keys)) - acknowledgeReturnDataOnItsWay(isOnItsWay: false) - return false - } - - guard let buffer = texture.buffer else { - os_log("(mglCommandInterface) Unable to access buffer of texture %{public}@", log: .default, type: .error, String(describing: texture)) - acknowledgeReturnDataOnItsWay(isOnItsWay: false) - return false - } - - let imageRowByteCount = Int(mglSizeOfFloatRgbaTexture(mglUInt32(texture.width), 1)) - acknowledgeReturnDataOnItsWay(isOnItsWay: true) - _ = commandInterface.writeUInt32(data: mglUInt32(texture.width)) - _ = commandInterface.writeUInt32(data: mglUInt32(texture.height)) - let totalByteCount = commandInterface.imageRowsFromBuffer(buffer: buffer, imageRowByteCount: imageRowByteCount, alignedRowByteCount: texture.bufferBytesPerRow, rowCount: texture.height) - - return totalByteCount == imageRowByteCount * texture.height - } - - func startStencilCreation(view: MTKView) -> Bool { - guard let stencilNumber = commandInterface.readUInt32(), - let isInverted = commandInterface.readUInt32() else { - return false - } - - let stencilIndex = Array.Index(stencilNumber) - if (!createStencilConfig.indices.contains(stencilIndex)) { - os_log("(mglRenderer) Got stencil number to create %{public}d but only numbers 0-7 are supported.", log: .default, type: .error, stencilNumber) - return false - } - - os_log("(mglRenderer) Creating stencil number %{public}d, with isInverted %{public}d.", log: .default, type: .info, stencilNumber, isInverted) - currentDepthStencilConfig = (isInverted != 0) ? createInvertedStencilConfig[stencilIndex] : createStencilConfig[stencilIndex] - return true - } - - func finishStencilCreation(view: MTKView) -> Bool { - os_log("(mglRenderer) Finishing stencil creation.", log: .default, type: .info) - currentDepthStencilConfig = enableDepthAndStencilConfig[0] - return true - } - - func selectStencil(view: MTKView, renderEncoder: MTLRenderCommandEncoder) -> Bool { - guard let stencilNumber = commandInterface.readUInt32() else { - return false - } - - let stencilIndex = Array.Index(stencilNumber) - if (!enableDepthAndStencilConfig.indices.contains(stencilIndex)) { - os_log("(mglRenderer) Got stencil number to select %{public}d but only numbers 0-7 are supported.", log: .default, type: .error, stencilNumber) - return false - } - - os_log("(mglRenderer) Selecting stencil number %{public}d.", log: .default, type: .info, stencilNumber) - currentDepthStencilConfig = enableDepthAndStencilConfig[stencilIndex] - currentDepthStencilConfig.configureRenderEncoder(renderEncoder: renderEncoder) - return true - } - - func updateTexture() -> Bool { - guard let textureNumber = commandInterface.readUInt32(), - let textureWidth = commandInterface.readUInt32(), - let textureHeight = commandInterface.readUInt32() else { - return false - } - - // Resolve the texture and its buffer. - guard let texture = textures[textureNumber] else { - os_log("(mglRenderer) Invalid texture number %{public}d, valid numbers are %{public}@.", log: .default, type: .error, textureNumber, String(describing: textures.keys)) - return false - } - - guard let buffer = texture.buffer else { - os_log("(mglRenderer) Texture has no buffer to update: %{public}@", log: .default, type: .error, String(describing: texture)) - return false - } - - // Read the actual image data into the texture. - let imageRowByteCount = Int(mglSizeOfFloatRgbaTexture(textureWidth, 1)) - let totalByteCount = commandInterface.imageRowsToBuffer(buffer: buffer, imageRowByteCount: imageRowByteCount, alignedRowByteCount: texture.bufferBytesPerRow, rowCount: Int(textureHeight)) - - return totalByteCount == imageRowByteCount * Int(textureHeight) - } - - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - // Drawing commands - //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ - class func dotsPipelineStateDescriptor(colorPixelFormat: MTLPixelFormat, depthPixelFormat: MTLPixelFormat, stencilPixelFormat: MTLPixelFormat, library: MTLLibrary?) -> MTLRenderPipelineDescriptor { - let pipelineDescriptor = MTLRenderPipelineDescriptor() - pipelineDescriptor.depthAttachmentPixelFormat = depthPixelFormat - pipelineDescriptor.stencilAttachmentPixelFormat = stencilPixelFormat - pipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat - pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true; - pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add; - pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add; - pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.sourceAlpha; - pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.sourceAlpha; - pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; - pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; - - let vertexDescriptor = MTLVertexDescriptor() - vertexDescriptor.attributes[0].format = .float3 - vertexDescriptor.attributes[0].offset = 0 - vertexDescriptor.attributes[0].bufferIndex = 0 - vertexDescriptor.attributes[1].format = .float4 - vertexDescriptor.attributes[1].offset = 3 * MemoryLayout.size - vertexDescriptor.attributes[1].bufferIndex = 0 - vertexDescriptor.attributes[2].format = .float2 - vertexDescriptor.attributes[2].offset = 7 * MemoryLayout.size - vertexDescriptor.attributes[2].bufferIndex = 0 - vertexDescriptor.attributes[3].format = .float - vertexDescriptor.attributes[3].offset = 9 * MemoryLayout.size - vertexDescriptor.attributes[3].bufferIndex = 0 - vertexDescriptor.attributes[4].format = .float - vertexDescriptor.attributes[4].offset = 10 * MemoryLayout.size - vertexDescriptor.attributes[4].bufferIndex = 0 - vertexDescriptor.layouts[0].stride = 11 * MemoryLayout.size - pipelineDescriptor.vertexDescriptor = vertexDescriptor - pipelineDescriptor.vertexFunction = library?.makeFunction(name: "vertex_dots") - pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "fragment_dots") - - return pipelineDescriptor - } - - func drawDots(view: MTKView, renderEncoder: MTLRenderCommandEncoder) -> Bool { - guard let (vertexBufferDots, vertexCount) = commandInterface.readVertices(device: mglRenderer.device, extraVals: 8) else { - return false - } - - // Draw all the vertices as points with 11 values per vertex: [xyz rgba wh isRound borderSize]. - renderEncoder.setRenderPipelineState(currentColorRenderingConfig.dotsPipelineState) - renderEncoder.setVertexBuffer(vertexBufferDots, offset: 0, index: 0) - renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: vertexCount) - return true - } - - class func arcsPipelineStateDescriptor(colorPixelFormat: MTLPixelFormat, depthPixelFormat: MTLPixelFormat, stencilPixelFormat: MTLPixelFormat, library: MTLLibrary?) -> MTLRenderPipelineDescriptor { - let pipelineDescriptor = MTLRenderPipelineDescriptor() - pipelineDescriptor.depthAttachmentPixelFormat = depthPixelFormat - pipelineDescriptor.stencilAttachmentPixelFormat = stencilPixelFormat - pipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat - pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true; - pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add; - pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add; - pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.sourceAlpha; - pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.sourceAlpha; - pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; - pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; - - let vertexDescriptor = MTLVertexDescriptor() - // xyz - vertexDescriptor.attributes[0].format = .float3 - vertexDescriptor.attributes[0].offset = 0 - vertexDescriptor.attributes[0].bufferIndex = 0 - // rgba - vertexDescriptor.attributes[1].format = .float4 - vertexDescriptor.attributes[1].offset = 3 * MemoryLayout.stride - vertexDescriptor.attributes[1].bufferIndex = 0 - // radii - vertexDescriptor.attributes[2].format = .float4 - vertexDescriptor.attributes[2].offset = 7 * MemoryLayout.stride - vertexDescriptor.attributes[2].bufferIndex = 0 - // wedge - vertexDescriptor.attributes[3].format = .float2 - vertexDescriptor.attributes[3].offset = 11 * MemoryLayout.stride - vertexDescriptor.attributes[3].bufferIndex = 0 - // border - vertexDescriptor.attributes[4].format = .float - vertexDescriptor.attributes[4].offset = 13 * MemoryLayout.stride - vertexDescriptor.attributes[4].bufferIndex = 0 - // center vertex (computed) - vertexDescriptor.attributes[5].format = .float3 - vertexDescriptor.attributes[5].offset = 14 * MemoryLayout.stride - vertexDescriptor.attributes[5].bufferIndex = 0 - // viewport size - vertexDescriptor.attributes[6].format = .float2 - vertexDescriptor.attributes[6].offset = 17 * MemoryLayout.stride - vertexDescriptor.attributes[6].bufferIndex = 0 - vertexDescriptor.layouts[0].stride = 19 * MemoryLayout.stride - pipelineDescriptor.vertexDescriptor = vertexDescriptor - pipelineDescriptor.vertexFunction = library?.makeFunction(name: "vertex_arcs") - pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "fragment_arcs") - - return pipelineDescriptor - } - - func drawArcs(view: MTKView, renderEncoder: MTLRenderCommandEncoder) -> Bool { - // read the center vertex for the arc from the commandInterface - // extra values are rgba (1x4), radii (1x4), wedge (1x2), border (1x1) - guard let (centerVertex, arcCount) = commandInterface.readVertices(device: mglRenderer.device, extraVals: 11) else { - return false - } - - // get an MTLBuffer from the GPU for storing the vertices for two triangles (i.e. - // we are going to make a square around where the arc is going to be drawn, and - // then color the pixels in the fragment shader according to how far they are away - // from the center.) Note that the vertices will have 3 + 2 more values than the - // centerVertex passed in, because each of these vertices will get the xyz of the - // centerVertex added on (which is used for the calculation for how far away each - // pixel is from the center in the fragment shader) and the viewport dimensions - let byteCount = 6 * ((centerVertex.length/arcCount) + 5 * MemoryLayout.stride); - guard let triangleVertices = mglRenderer.device.makeBuffer(length: byteCount * arcCount, options: .storageModeManaged) else { - os_log("(mglRenderer:drawArcs) Could not make vertex buffer of size %{public}d", log: .default, type: .error, byteCount) - return false - } - - // get size of buffer as number of floats, note that we add - // 3 floats for the center position plus 2 floats for the viewport dimensions - let vertexBufferSize = 5 + (centerVertex.length/arcCount)/MemoryLayout.stride; - - // get pointers to the buffer that we will pass to the renderer - let triangleVerticesPointer = triangleVertices.contents().assumingMemoryBound(to: Float.self); - - // get the viewport size, which may be the on-screen view or an offscreen texture - let (viewportWidth, viewportHeight) = currentColorRenderingConfig.getSize(view: view) - - // iterate over how many vertices (i.e. how many arcs) that the user passed in - for iArc in 0...stride - // Now create the vertices of each corner of the triangles by copying - // the centerVertex in and then modifying the x, y location appropriately - // get desired x and y locations of the triangle corners - let x = centerVertexPointer[0]; - let y = centerVertexPointer[1]; - // radius is the outer radius + half the border - let rX = centerVertexPointer[8]+centerVertexPointer[13]/2; - let rY = centerVertexPointer[10]+centerVertexPointer[13]/2; - let xLocs: [Float] = [x-rX, x-rX, x+rX, x-rX, x+rX, x+rX] - let yLocs: [Float] = [y-rY, y+rY, y+rY, y-rY, y-rY, y+rY] - - // iterate over 6 vertices (which will be the corners of the triangles) - for iVertex in 0...5 { - // get a pointer to the location in the triangleVertices where we want to copy into - let thisTriangleVerticesPointer = triangleVerticesPointer + iVertex*vertexBufferSize + iArc*vertexBufferSize*6; - // and copy the center vertex into each location - memcpy(thisTriangleVerticesPointer, centerVertexPointer, centerVertex.length/arcCount); - // now set the xy location - thisTriangleVerticesPointer[0] = xLocs[iVertex]; - thisTriangleVerticesPointer[1] = yLocs[iVertex]; - // and set the centerVertex - thisTriangleVerticesPointer[14] = centerVertexPointer[0] - thisTriangleVerticesPointer[15] = -centerVertexPointer[1] - thisTriangleVerticesPointer[16] = centerVertexPointer[2] - // and set viewport dimension - thisTriangleVerticesPointer[17] = viewportWidth - thisTriangleVerticesPointer[18] = viewportHeight - } - - } - // Draw all the arcs - renderEncoder.setRenderPipelineState(currentColorRenderingConfig.arcsPipelineState) - renderEncoder.setVertexBuffer(triangleVertices, offset: 0, index: 0) - renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6*arcCount) - return true - } - - - class func bltTexturePipelineStateDescriptor(colorPixelFormat: MTLPixelFormat, depthPixelFormat: MTLPixelFormat, stencilPixelFormat: MTLPixelFormat, library: MTLLibrary?) -> MTLRenderPipelineDescriptor { - let pipelineDescriptor = MTLRenderPipelineDescriptor() - pipelineDescriptor.depthAttachmentPixelFormat = depthPixelFormat - pipelineDescriptor.stencilAttachmentPixelFormat = stencilPixelFormat - pipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat - pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true; - pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add; - pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add; - pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.sourceAlpha; - pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.sourceAlpha; - pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; - pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; - - let vertexDescriptor = MTLVertexDescriptor() - vertexDescriptor.attributes[0].format = .float3 - vertexDescriptor.attributes[0].offset = 0 - vertexDescriptor.attributes[0].bufferIndex = 0 - vertexDescriptor.attributes[1].format = .float2 - vertexDescriptor.attributes[1].offset = 3 * MemoryLayout.size - vertexDescriptor.attributes[1].bufferIndex = 0 - vertexDescriptor.layouts[0].stride = 5 * MemoryLayout.size - pipelineDescriptor.vertexDescriptor = vertexDescriptor - pipelineDescriptor.vertexFunction = library?.makeFunction(name: "vertex_textures") - pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "fragment_textures") - - return pipelineDescriptor - } - - func bltTexture(view: MTKView, renderEncoder: MTLRenderCommandEncoder) -> Bool { - // Read all data up front, since it's expected to be consumed. - guard let minMagFilterRawValue = commandInterface.readUInt32(), - let mipFilterRawValue = commandInterface.readUInt32(), - let addressModeRawValue = commandInterface.readUInt32(), - let (vertexBufferTexture, vertexCount) = commandInterface.readVertices(device: mglRenderer.device, extraVals: 2), - var phase = commandInterface.readFloat(), - let textureNumber = commandInterface.readUInt32() else { - return false - } - - // Make sure we have the actual requested texture. - guard let texture = textures[textureNumber] else { - os_log("(mglRenderer) Invalid texture number %{public}d, valid numbers are %{public}@.", log: .default, type: .error, textureNumber, String(describing: textures.keys)) - return false - } - - // Set up texture sampling and filtering. - let minMagFilter = chooseMinMagFilter(rawValue: UInt(minMagFilterRawValue)) - let mipFilter = chooseMipFilter(rawValue: UInt(mipFilterRawValue)) - let addressMode = chooseAddressMode(rawValue: UInt(addressModeRawValue)) - let samplerDescriptor = MTLSamplerDescriptor() - samplerDescriptor.minFilter = minMagFilter - samplerDescriptor.magFilter = minMagFilter - samplerDescriptor.mipFilter = mipFilter - samplerDescriptor.sAddressMode = addressMode - samplerDescriptor.tAddressMode = addressMode - samplerDescriptor.rAddressMode = addressMode - let samplerState = mglRenderer.device.makeSamplerState(descriptor:samplerDescriptor) - - // Draw vertices as points with 5 values per vertex: [xyz uv]. - renderEncoder.setRenderPipelineState(currentColorRenderingConfig.texturePipelineState) - renderEncoder.setVertexBuffer(vertexBufferTexture, offset: 0, index: 0) - renderEncoder.setFragmentSamplerState(samplerState, index: 0) - renderEncoder.setFragmentBytes(&phase, length: MemoryLayout.stride, index: 2) - renderEncoder.setFragmentTexture(texture, index:0) - renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount) - - return true - } - - func chooseMinMagFilter(rawValue: UInt, defaultValue: MTLSamplerMinMagFilter = .linear) -> MTLSamplerMinMagFilter { - guard let filter = MTLSamplerMinMagFilter(rawValue: rawValue) else { - return defaultValue - } - return filter - } - - func chooseMipFilter(rawValue: UInt, defaultValue: MTLSamplerMipFilter = .linear) -> MTLSamplerMipFilter { - guard let filter = MTLSamplerMipFilter(rawValue: rawValue) else { - return defaultValue - } - return filter - } - - func chooseAddressMode(rawValue: UInt, defaultValue: MTLSamplerAddressMode = .repeat) -> MTLSamplerAddressMode { - guard let filter = MTLSamplerAddressMode(rawValue: rawValue) else { - return defaultValue - } - return filter - } - - class func drawVerticesPipelineStateDescriptor(colorPixelFormat: MTLPixelFormat, depthPixelFormat: MTLPixelFormat, stencilPixelFormat: MTLPixelFormat, library: MTLLibrary?) -> MTLRenderPipelineDescriptor { - let pipelineDescriptor = MTLRenderPipelineDescriptor() - pipelineDescriptor.depthAttachmentPixelFormat = depthPixelFormat - pipelineDescriptor.stencilAttachmentPixelFormat = stencilPixelFormat - pipelineDescriptor.colorAttachments[0].pixelFormat = colorPixelFormat - pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true; - pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperation.add; - pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperation.add; - pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactor.sourceAlpha; - pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactor.sourceAlpha; - pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; - pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactor.oneMinusSourceAlpha; - - let vertexDescriptor = MTLVertexDescriptor() - vertexDescriptor.attributes[0].format = .float3 - vertexDescriptor.attributes[0].offset = 0 - vertexDescriptor.attributes[0].bufferIndex = 0 - vertexDescriptor.attributes[1].format = .float3 - vertexDescriptor.attributes[1].offset = 3 * MemoryLayout.size - vertexDescriptor.attributes[1].bufferIndex = 0 - vertexDescriptor.layouts[0].stride = 6 * MemoryLayout.size - pipelineDescriptor.vertexDescriptor = vertexDescriptor - pipelineDescriptor.vertexFunction = library?.makeFunction(name: "vertex_with_color") - pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "fragment_with_color") - - return pipelineDescriptor - } - - func drawVerticesWithColor(view: MTKView, renderEncoder: MTLRenderCommandEncoder, primitiveType: MTLPrimitiveType) -> Bool { - guard let (vertexBufferWithColors, vertexCount) = commandInterface.readVertices(device: mglRenderer.device, extraVals: 3) else { - return false - } - - // Render vertices as points with 6 values per vertex: [xyz rgb] - renderEncoder.setRenderPipelineState(currentColorRenderingConfig.verticesWithColorPipelineState) - renderEncoder.setVertexBuffer(vertexBufferWithColors, offset: 0, index: 0) - renderEncoder.drawPrimitives(type: primitiveType, vertexStart: 0, vertexCount: vertexCount) - return true - } - - func setXform(renderEncoder: MTLRenderCommandEncoder) -> Bool { - guard let newDeg2Metal = commandInterface.readXform() else { - return false - } - deg2metal = newDeg2Metal - - // Attach this new view transform to the same location expected by all vertex shaders (our convention). - renderEncoder.setVertexBytes(°2metal, length: MemoryLayout.stride, index: 1) - return true - } - - func repeatFlicker(view: MTKView, renderEncoder: MTLRenderCommandEncoder) -> Bool { - if (repeatingCommandFrameCount == 0) { - guard let repeatCount = commandInterface.readUInt32() else { - return false - } - repeatingCommandFrameCount = UInt32(repeatCount) - - guard let randomSeed = commandInterface.readUInt32() else { - return false - } - repeatingCommandRandomSouce = GKMersenneTwisterRandomSource(seed: UInt64(randomSeed)) - - repeatingCommandCode = mglRepeatFlicker - } - - // Choose a new, random color for the view to use on the next render pass. - let r = Double(repeatingCommandRandomSouce.nextUniform()) - let g = Double(repeatingCommandRandomSouce.nextUniform()) - let b = Double(repeatingCommandRandomSouce.nextUniform()) - let clearColor = MTLClearColor(red: r, green: g, blue: b, alpha: 1) - view.clearColor = clearColor - - return true - } - - func repeatBlts(view: MTKView, renderEncoder: MTLRenderCommandEncoder) -> Bool { - if (repeatingCommandFrameCount == 0) { - guard let repeatCount = commandInterface.readUInt32() else { - return false - } - repeatingCommandFrameCount = UInt32(repeatCount) - - repeatingCommandCode = mglRepeatBlts - } - - // For now, choose arbitrary vertices to blt onto. - let vertexByteCount = Int(mglSizeOfFloatVertexArray(6, 5)) - guard let vertexBuffer = mglRenderer.device.makeBuffer(length: vertexByteCount, options: .storageModeManaged) else { - os_log("(mglRenderer) Could not make vertex buffer of size %{public}d", log: .default, type: .error, vertexByteCount) - return false - } - let vertexData: [Float32] = [ - 1, 1, 0, 1, 0, - -1, 1, 0, 0, 0, - -1, -1, 0, 0, 1, - 1, 1, 0, 1, 0, - -1, -1, 0, 0, 1, - 1, -1, 0, 1, 1 - ] - let bufferFloats = vertexBuffer.contents().bindMemory(to: Float32.self, capacity: vertexData.count) - bufferFloats.update(from: vertexData, count: vertexData.count) - - // Choose a next texture from the available textures, varying with the repeating command count. - let textureNumbers = Array(textures.keys).sorted() - let textureIndex = Int(repeatingCommandFrameCount) % textureNumbers.count - let textureNumber = textureNumbers[textureIndex] - guard let texture = textures[textureNumber] else { - os_log("(mglRenderer) Invalid texture number %{public}d, valid numbers are %{public}@.", log: .default, type: .error, textureNumber, String(describing: textures.keys)) - return false - } - - // For now, choose an arbitrary, fixed sampling strategy. - let samplerDescriptor = MTLSamplerDescriptor() - samplerDescriptor.minFilter = .nearest - samplerDescriptor.magFilter = .nearest - samplerDescriptor.mipFilter = .nearest - samplerDescriptor.sAddressMode = .repeat - samplerDescriptor.tAddressMode = .repeat - samplerDescriptor.rAddressMode = .repeat - let samplerState = mglRenderer.device.makeSamplerState(descriptor:samplerDescriptor) - - // For now, assume drift-phase 0. - var phase = Float32(0) - - renderEncoder.setRenderPipelineState(currentColorRenderingConfig.texturePipelineState) - renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) - renderEncoder.setFragmentSamplerState(samplerState, index: 0) - renderEncoder.setFragmentBytes(&phase, length: MemoryLayout.stride, index: 2) - renderEncoder.setFragmentTexture(texture, index:0) - renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) - - return true - } - - func repeatQuads(view: MTKView, renderEncoder: MTLRenderCommandEncoder) -> Bool { - if (repeatingCommandFrameCount == 0) { - guard let repeatCount = commandInterface.readUInt32() else { - return false - } - repeatingCommandFrameCount = UInt32(repeatCount) - - guard let objectCount = commandInterface.readUInt32() else { - return false - } - repeatingCommandObjectCount = UInt32(objectCount) - - guard let randomSeed = commandInterface.readUInt32() else { - return false - } - repeatingCommandRandomSouce = GKMersenneTwisterRandomSource(seed: UInt64(randomSeed)) - - repeatingCommandCode = mglRepeatQuads - } - - // Pack a vertex buffer with quads: each has 6 vertices (two triangels) and 6 values per vertex [xyz rgb]. - let vertexCount = Int(6 * repeatingCommandObjectCount) - let byteCount = Int(mglSizeOfFloatVertexArray(mglUInt32(vertexCount), 6)) - guard let vertexBuffer = mglRenderer.device.makeBuffer(length: byteCount, options: .storageModeManaged) else { - os_log("(mglRenderer) Could not make vertex buffer of size %{public}d", log: .default, type: .error, byteCount) - return false - } - let bufferFloats = vertexBuffer.contents().bindMemory(to: Float32.self, capacity: vertexCount * 6) - for quadIndex in (0 ..< repeatingCommandObjectCount) { - let offset = Int(6 * 6 * quadIndex) - packRandomQuad(buffer: bufferFloats, offset: offset) - } - - // The buffer here uses storageModeManaged, to match the behavior of mglCommandInterface. - // This means we have to tell the GPU about the modifications we just made using the CPU. - vertexBuffer.didModifyRange(0 ..< byteCount) - - // Render vertices as triangles, two per quad, and 6 values per vertex: [xyz rgb]. - renderEncoder.setRenderPipelineState(currentColorRenderingConfig.verticesWithColorPipelineState) - renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) - renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexCount) - return true - } - - // Create a random quad as the next 6 triangle vertices ([xyz rgb], so 36 elements total) of the given vertex buffer. - private func packRandomQuad(buffer: UnsafeMutablePointer, offset: Int) { - // Pick four random corners of the quad, vertices 0, 1, 2, 3. - let x0 = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - let x1 = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - let x2 = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - let x3 = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - let y0 = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - let y1 = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - let y2 = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - let y3 = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - - // Pick one random color for the whole quad. - let r = Float32(repeatingCommandRandomSouce.nextUniform()) - let g = Float32(repeatingCommandRandomSouce.nextUniform()) - let b = Float32(repeatingCommandRandomSouce.nextUniform()) - - // First triangle of the quad gets vertices, 0, 1, 2. - buffer[offset + 0] = x0 - buffer[offset + 1] = y0 - buffer[offset + 2] = 0 - buffer[offset + 3] = r - buffer[offset + 4] = g - buffer[offset + 5] = b - buffer[offset + 6] = x1 - buffer[offset + 7] = y1 - buffer[offset + 8] = 0 - buffer[offset + 9] = r - buffer[offset + 10] = g - buffer[offset + 11] = b - buffer[offset + 12] = x2 - buffer[offset + 13] = y2 - buffer[offset + 14] = 0 - buffer[offset + 15] = r - buffer[offset + 16] = g - buffer[offset + 17] = b - - // Second triangle of the quad gets vertices, 2, 1, 3. - buffer[offset + 18] = x2 - buffer[offset + 19] = y2 - buffer[offset + 20] = 0 - buffer[offset + 21] = r - buffer[offset + 22] = g - buffer[offset + 23] = b - buffer[offset + 24] = x1 - buffer[offset + 25] = y1 - buffer[offset + 26] = 0 - buffer[offset + 27] = r - buffer[offset + 28] = g - buffer[offset + 29] = b - buffer[offset + 30] = x3 - buffer[offset + 31] = y3 - buffer[offset + 32] = 0 - buffer[offset + 33] = r - buffer[offset + 34] = g - buffer[offset + 35] = b - } - - func repeatDots(view: MTKView, renderEncoder: MTLRenderCommandEncoder) -> Bool { - if (repeatingCommandFrameCount == 0) { - guard let repeatCount = commandInterface.readUInt32() else { - return false - } - repeatingCommandFrameCount = UInt32(repeatCount) - - guard let objectCount = commandInterface.readUInt32() else { - return false - } - repeatingCommandObjectCount = UInt32(objectCount) - - guard let randomSeed = commandInterface.readUInt32() else { - return false - } - repeatingCommandRandomSouce = GKMersenneTwisterRandomSource(seed: UInt64(randomSeed)) - - repeatingCommandCode = mglRepeatDots - } - - // Pack a vertex buffer with dots: each has 1 vertex and 11 values per vertex vertex: [xyz rgba wh isRound borderSize]. - let vertexCount = Int(repeatingCommandObjectCount) - let byteCount = Int(mglSizeOfFloatVertexArray(mglUInt32(vertexCount), 11)) - guard let vertexBuffer = mglRenderer.device.makeBuffer(length: byteCount, options: .storageModeManaged) else { - os_log("(mglRenderer) Could not make vertex buffer of size %{public}d", log: .default, type: .error, byteCount) - return false - } - let bufferFloats = vertexBuffer.contents().bindMemory(to: Float32.self, capacity: vertexCount) - for dotIndex in (0 ..< vertexCount) { - let offset = Int(11 * dotIndex) - packRandomDot(buffer: bufferFloats, offset: offset) - } - - // Draw all the vertices as points with 11 values per vertex: [xyz rgba wh isRound borderSize]. - renderEncoder.setRenderPipelineState(currentColorRenderingConfig.dotsPipelineState) - renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) - renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: vertexCount) - return true - } - - // Create a random dot as the next vertex, with 11 elements per vertex, of the given vertex buffer. - private func packRandomDot(buffer: UnsafeMutablePointer, offset: Int) { - // xyz - buffer[offset + 0] = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - buffer[offset + 1] = Float32(repeatingCommandRandomSouce.nextUniform() * 2 - 1) - buffer[offset + 2] = 0 - - // rgba - buffer[offset + 3] = Float32(repeatingCommandRandomSouce.nextUniform()) - buffer[offset + 4] = Float32(repeatingCommandRandomSouce.nextUniform()) - buffer[offset + 5] = Float32(repeatingCommandRandomSouce.nextUniform()) - buffer[offset + 6] = 1 - - // wh - buffer[offset + 7] = 1 - buffer[offset + 8] = 1 - - // round - buffer[offset + 9] = 0 - - // border size - buffer[offset + 10] = 0 - } - - func repeatFlush(view: MTKView, renderEncoder: MTLRenderCommandEncoder) -> Bool { - if (repeatingCommandFrameCount == 0) { - guard let repeatCount = commandInterface.readUInt32() else { - return false - } - repeatingCommandFrameCount = UInt32(repeatCount) - - repeatingCommandCode = mglRepeatFlush - } - - // This is a no-op, the only thing this needed to do was set the repeatingCommandFrameCount, above. - return true - } -} diff --git a/metal/mglMetal/mglRenderer2.swift b/metal/mglMetal/mglRenderer2.swift new file mode 100644 index 00000000..6ba124ed --- /dev/null +++ b/metal/mglMetal/mglRenderer2.swift @@ -0,0 +1,576 @@ +// +// mglRenderer2.swift +// mglMetal +// +// Created by Benjamin Heasly on 12/22/23. +// Copyright © 2023 GRU. All rights reserved. +// + +import Foundation +import MetalKit + +/* + This mglRenderer2 processes commands and handles details of setting up Metal rendering passes for each frame. + This takes unprocessed commands from our mglCommandInterface, but doesn't know about how the commands were created. + This processes each command in a few steps, but doesn't worry about the details of each command: + - doNondrawingWork() is a chance for each command to modify the state of this app and/or gather data about the system. + - draw() is a chance for each command to draw itself as part of a rendering pass / frame. + - this fills in success status and timing results around each command + This reports completed commands back to our mglCommandInterface, but doesn't know what happens from there. + */ +class mglRenderer2: NSObject { + // A logging interface that handles macOS version dependency and remembers the last error message. + private let logger: mglLogger + + // GPU Device! + private let device: MTLDevice + + // GPU and CPU buffers where we can gather and read out detailed redering pipeline timestamps (if GPU supports). + // We configure render passes to store timestamps in the GPU buffer, then explicitly blit the results to the CPU buffer. + // There's supposed to be an easier API with one shared GPU buffer, but it seems not to work in general -- even on an M1 iMac! + // https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/converting_a_gpu_s_counter_data_into_a_readable_format + private let gpuTimestampBuffer: MTLCounterSampleBuffer? + private let cpuTimestampBuffer: MTLBuffer? + + // Utility to get system nano time. + let secs = mglSecs() + + // The Metal commandQueue tells the device what to do. + private let commandQueue: MTLCommandQueue! + + // Our command interface communicates with the client process like Matlab. + private let commandInterface: mglCommandInterface + + // Keep track of one frame in-flight at a time. + private var flushInFlight: mglCommand? = nil + private var flushInFlightSemaphore = DispatchSemaphore(value: 0) + private var lastPresentedTime: CFTimeInterval = 0.0 + + // Keep track of depth and stencil state, like creating vs applying a stencil, and which stencil. + private let depthStencilState: mglDepthStencilState + + // Keep track of color rendering state, like oncreen vs offscreen texture, and which texture. + private let colorRenderingState: mglColorRenderingState + + // Keeps the current coordinate xform specified by the client, like screen pixels vs device visual degrees. + private var deg2metal = matrix_identity_float4x4 + + init(logger: mglLogger, metalView: MTKView, commandInterface: mglCommandInterface) { + self.logger = logger + self.commandInterface = commandInterface + + // Initialize the GPU device. + guard let device = MTLCreateSystemDefaultDevice() else { + fatalError("GPU not available") + } + self.device = device + metalView.device = device + + // Try to set up buffers to holder detailed pipeline stage timestamps. + // The timestamps we want are "stage boundary" timestamps before and after the vertex and fragment pipeline stages. + // Not all OS versions and GPUs support these timestamps. + let supportsStageBoundaryTimestamps = gpuSupportsStageBoundaryTimestamps(logger: logger, device: device) + + // We also need to check whether the GPU is capable of repording timestamps to a buffer at all. + let timestampCounterSet = getGpuTimestampCounter(logger: logger, device: device) + + if supportsStageBoundaryTimestamps && timestampCounterSet != nil { + // OK we can try to set up buffers for detailed pipeline timestamps. + gpuTimestampBuffer = makeGpuTimestampBuffer(logger: logger, device: device, counterSet: timestampCounterSet!) + cpuTimestampBuffer = makeCpuTimestampBuffer(logger: logger, device: device) + if gpuTimestampBuffer != nil && cpuTimestampBuffer != nil { + logger.info(component: "mglRenderer2", + details: "OK -- Created GPU buffer to collect detailed pipeline stage timestamps.") + } else { + logger.info(component: "mglRenderer2", + details: "Could not create GPU and CPU timestamp buffers -- frame times will be best-effort.") + } + } else { + // We'll have to fall back on best-effort CPU timestamps. + gpuTimestampBuffer = nil + cpuTimestampBuffer = nil + logger.info(component: "mglRenderer2", + details: "GPU device does not support detailed pipeline stage timestmps, frame times will be best-effort.") + } + + // Initialize the low-level Metal command queue. + commandQueue = device.makeCommandQueue()! + + // Inititialize 8 stencil planes and depth testing. + metalView.depthStencilPixelFormat = .depth32Float_stencil8 + metalView.clearDepth = 1.0 + depthStencilState = mglDepthStencilState(logger: logger, device: device) + + // Initialize color rendering, default to onscreen. + // Default gray clear color applies to onscreen presentation and offscreen rendering to texture. + metalView.clearColor = MTLClearColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1.0) + colorRenderingState = mglColorRenderingState(logger: logger, device: device, view: metalView) + + // init the super class + super.init() + + // Tell the view that this class will be used as the delegate for draw() and resize() callbacks. + metalView.delegate = self + + // Get some info about the underlying layer and drawables. + let layer = metalView.layer as? CAMetalLayer + if layer != nil { + logger.info(component: "mglRenderer2", details: "View's CAMetalLayer has maximumDrawableCount: \(layer!.maximumDrawableCount).") + logger.info(component: "mglRenderer2", details: "View's CAMetalLayer has displaySyncEnabled: \(layer!.displaySyncEnabled).") + } + + logger.info(component: "mglRenderer2", details: "Init OK.") + } +} + +extension mglRenderer2: MTKViewDelegate { + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + logger.info(component: "mglRenderer2", details: "drawableSizeWillChange \(String(describing: size))") + } + + // We expect the view to be configured for "Timed updates" (the default), + // which is the traditional way of running once per "video frame", "screen refresh", etc. + // as described here: + // https://developer.apple.com/documentation/metalkit/mtkview?language=objc + func draw(in view: MTKView) { + // Using autoreleasepool lets us attempt to release system resources like the "drawable" as soon as we're done. + // Apple's guidance is to do this once per frame, rather than letting it happen lazily at some later time. + // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html + autoreleasepool { + render(in: view) + } + } + + // This is called by the system on a frame-by-frame schedule. + private func render(in view: MTKView) { + // Resolve the current flush command in-flight, if any. + // This could involve a short wait to make sure: + // - any previous renders to texture are done and synced for CPU access like reading / frame grabbing. + // - any GPU timestamp samples are synced for CPU access and reporting. + // We don't expect to wait much, if at all. + // But this is a sync point for consistency betweeh CPU and GPU, which in general are asynchronous. + if flushInFlight != nil { + waitForFlushInFlight() + commandInterface.done(command: flushInFlight!) + flushInFlight = nil + } + + // Let the command interface read new commands from the client, if any. + // This will wait up to a new milliseconds before timing out. + // Waiting a little is good here: it gives the client a chance to compute and send the next command. + // If we didn't wait here, the client might miss its chance to draw into this frame, + // so we'd drop this frame and wait all the way until the next frame before reading the next command. + // However, we dont' want to wait forever, so this won't block indefinitely. + commandInterface.readAny(device: device) + + // Get the next command to be processed from the command interface, if any. + // If we don't get one this time, that's fine, we'll check again on the next frame. + guard var command = commandInterface.next() else { + return + } + + // Let the command do non-drawing work, like getting and setting the state of the app. + let nondrawingSuccess = command.doNondrawingWork( + logger: logger, + view: view, + depthStencilState: depthStencilState, + colorRenderingState: colorRenderingState, + deg2metal: °2metal + ) + + // On failure, exit right away. + if !nondrawingSuccess { + commandInterface.done(command: command, success: false) + return + } + + // On success, check whether the command also wants to draw something. + if command.framesRemaining <= 0 { + // No "frames remaining" means that this nondrawing command is all done. + commandInterface.done(command: command) + return + } + + // Yes "frames remaining" means that this command wants to draw something. So: + // 1. Set up a Metal rendering pass. + // 2. Enter a tight loop to accept more commands in the same frame. + // 3. Present the frame representing all commands until "flush". + + // + // 1. Set up a Metal rendering pass. + // + + // This call to view.currentDrawable accesses an expensive system resource, the "drawable". + // Normally this is fast and not a problem. + // But we've seen it get slow when using large amounts of video memory. + // For example when we have 30 full-screen-sized textures loaded at once. + // For rgba Float32 textures, 30 of these can take up about a GB of memory. + // In this case the call duration is sometimes <1ms, sometimes ~7ms, sometimes even ~14 ms. + // It's not entirely consistent, but the durations seem to come in runs that last for many frames or several seconds. + // Even when in a run of ~14ms durations, we don't necessarily drop more frames than usual. + // When we do drop a frame, this seems to kick off a new run with a different call duration, for a while. + // We may be seeing some blocking/synchronizing on the expensive "drawable" resource. + // We seem to be already following the guidance about the "drawable" as discussed in Apple docs: + // https://developer.apple.com/documentation/metalkit/mtkview + // https://developer.apple.com/documentation/quartzcore/cametallayer#3385893 + // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/Drawables.html + // In particular, we're doing our rendering work inside an autoreleasepool {} block, + // and we're not acquiring the drawable until we really are about to render a frame. + // So, are we just finding out how to tax the system and seeing what happens in that case? + // Does the system "know best" and we are blocking/synchronizing as expected? + // Or is there somethign else we can do about these long call durations? + guard let drawable = view.currentDrawable else { + logger.error(component: "mglRenderer2", details: "Could not get current drawable, aborting render pass.") + commandInterface.done(command: command, success: false) + return + } + + // Record how long it took to acquire the drawable in response to this drawing command. + let drawableAcquired = secs.get() + + // This call to getRenderPassDescriptor(view: view) internally calls view.currentRenderPassDescriptor. + // The call to view.currentRenderPassDescriptor impicitly accessed the view's currentDrawable, as mentioned above. + // It's possible to swap the order of these calls. + // But whichever one we call first seems to pay the same blocking/synchronization price when memory usage is high. + guard let renderPassDescriptor = colorRenderingState.getRenderPassDescriptor(view: view) else { + logger.error(component: "mglRenderer2", details: "Could not get render pass descriptor from current color rendering config, aborting render pass.") + commandInterface.done(command: command, success: false) + return + } + + depthStencilState.configureRenderPassDescriptor(renderPassDescriptor: renderPassDescriptor) + + // If supported by OS and GPU, set up to store detailed pipeline stage timestamps during the render pass. + // Later, when we have a flush command in hand, we'll read the timestamps into the flush command's results struct. + setUpRenderPassGpuTimestamps(renderPassDescriptor: renderPassDescriptor) + + // The command buffer and render encoder are how we instruct the GPU to draw things on each frame. + guard let commandBuffer = commandQueue.makeCommandBuffer(), + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + logger.error(component: "mglRenderer2", details: "Could not get command buffer and renderEncoder from the command queue, aborting render pass.") + commandInterface.done(command: command, success: false) + return + } + depthStencilState.configureRenderEncoder(renderEncoder: renderEncoder) + + // Attach our view transform to the same location expected by all vertex shaders (our convention). + renderEncoder.setVertexBytes(°2metal, length: MemoryLayout.stride, index: 1) + + // + // 2. Enter a tight loop to accept more commands in the same frame. + // + + while !(command is mglFlushCommand) { + // Here at the top of the tight loop, this command is either: + // - the command received above with framesRemaining > 0, which initiated this frame's tight loop + // - another command received below which is being processed as part of the same frame + command.results.drawableAcquired = drawableAcquired + let drawSuccess = command.draw( + logger: logger, + view: view, + depthStencilState: depthStencilState, + colorRenderingState: colorRenderingState, + deg2metal: °2metal, + renderEncoder: renderEncoder + ) + + // On failure, end the frame. + if !drawSuccess { + commandInterface.done(command: command, success: false) + renderEncoder.endEncoding() + colorRenderingState.finishDrawing(commandBuffer: commandBuffer, drawable: drawable) + commandBuffer.present(drawable) + commandBuffer.commit() + return + } + + // On success, count the command as having drawn a frame. + command.framesRemaining -= 1 + + // We have special case for "repeating" commands which draw across multiple frames. + if command.framesRemaining > 0 { + // Done adding drawing commands for this frame. + renderEncoder.endEncoding() + + // If supported by OS and GPU, resolve pipeline stage timestamps gethered during the render pass. + resolveRenderPassGpuTimestamps(commandBuffer: commandBuffer, command: command) + + // Set up this command as the flush command in-flight. + setUpFlushInFlight(drawable: drawable, commandBuffer: commandBuffer, command: command) + + // Present this frame. + colorRenderingState.finishDrawing(commandBuffer: commandBuffer, drawable: drawable) + commandBuffer.present(drawable) + commandBuffer.commit() + + // And also re-add this command so it will be the one processed on the next frame. + commandInterface.addNext(command: command) + return + } + + // Normal, non-repeating commands can just be done now. + commandInterface.done(command: command) + + // Tight loop: wait for the next command here in the same frame. + // At this point we assume the client has sent another command, or intends to soon. + // We don't want to exit the frame early on a race condition, so we are willing to block and wait. + commandInterface.awaitNext(device: device) + if let nextCommand = commandInterface.next() { + command = nextCommand + + // Let the next command get or set app state, even during the frame tight loop. + let nextNondrawingSuccess = command.doNondrawingWork( + logger: logger, + view: view, + depthStencilState: depthStencilState, + colorRenderingState: colorRenderingState, + deg2metal: °2metal + ) + + // On failure, end the frame. + if !nextNondrawingSuccess { + commandInterface.done(command: command, success: false) + renderEncoder.endEncoding() + colorRenderingState.finishDrawing(commandBuffer: commandBuffer, drawable: drawable) + commandBuffer.present(drawable) + commandBuffer.commit() + return + } + + } else { + // This is unexpected, maybe the client disconnected or sent bad data. + // Just end the frame. + commandInterface.done(command: command, success: false) + renderEncoder.endEncoding() + colorRenderingState.finishDrawing(commandBuffer: commandBuffer, drawable: drawable) + commandBuffer.present(drawable) + commandBuffer.commit() + return + } + } + + // + // 3. Present the frame representing all commands until "flush". + // + + // This command is the flush command that got us out of the frame tight loop. + command.results.drawableAcquired = drawableAcquired + command.framesRemaining -= 1 + + // Done adding drawing commands for this frame. + renderEncoder.endEncoding() + + // If supported by OS and GPU, resolve pipeline stage timestamps gethered during the render pass. + resolveRenderPassGpuTimestamps(commandBuffer: commandBuffer, command: command) + + // Set up this command as the flush command in-flight. + // We'll report this to the client at the start of the next frame's render() call. + setUpFlushInFlight(drawable: drawable, commandBuffer: commandBuffer, command: command) + + // Present this frame. + colorRenderingState.finishDrawing(commandBuffer: commandBuffer, drawable: drawable) + commandBuffer.present(drawable) + commandBuffer.commit() + } + + // Wait for a flush command in-flight, as set up by setUpFlushInFlight(). + // This waits until we know the command is complete and records a completion timesetamp. + private func waitForFlushInFlight() { + // Don't start the next command until the previous frame is done processing. + // We don't expect this wait to be long, if at all. + flushInFlightSemaphore.wait() + if #available(macOS 10.15.4, *) { + // The previous drawable.addPresentedHandler() should have filled presented time for the drawable. + flushInFlight?.results.drawablePresented = lastPresentedTime + } else { + // Make a best effort to record a presented time for the previous flush -- now. + flushInFlight?.results.drawablePresented = secs.get() + } + } + + // Set up a flush command as being in-flight, which means: + // The client may be blocked, waiting for this command to be complete. + // We won't report it as complete until we think the the drawable has been presented. + // We'll report this to the client at the start of the next frame's render() call. + private func setUpFlushInFlight(drawable: MTLDrawable, commandBuffer: MTLCommandBuffer, command: mglCommand) { + if #available(macOS 10.15.4, *) { + // If supported, let the system tell us when drawables / frames are presented. + drawable.addPresentedHandler({ [weak self] drawable in + guard let strongSelf = self else { + return + } + strongSelf.lastPresentedTime = drawable.presentedTime + }) + } + + // Setting flushInFlight will make us wait for completion at the start of the next frame's render() call. + // So, always pair setting flushInFlight with a semaphore signal(). + // In this case, let the system call back when the command is all done processing, and signal then. + // Note: "done processing" means ready to present on the screen, + // but actual presentation on the screen may happen later, on the system's schedule. + commandBuffer.addCompletedHandler { [flushInFlightSemaphore] commandBuffer in + flushInFlightSemaphore.signal() + } + self.flushInFlight = command + } + + /* + Set up the current render pass to record detailed pipeline stage timestamps, if supported by OS and GPU. + This only seems to be available on macOS 11.0 and later (Big Sur 2020). + */ + private func setUpRenderPassGpuTimestamps(renderPassDescriptor: MTLRenderPassDescriptor) { + guard let gpuTimestampBuffer = self.gpuTimestampBuffer else { + return + } + + if #available(macOS 11.0, *) { + guard let sampleAttachment = renderPassDescriptor.sampleBufferAttachments[0] else { + return + } + sampleAttachment.sampleBuffer = gpuTimestampBuffer + sampleAttachment.startOfVertexSampleIndex = 0 + sampleAttachment.endOfVertexSampleIndex = 1 + sampleAttachment.startOfFragmentSampleIndex = 2 + sampleAttachment.endOfFragmentSampleIndex = 3 + } + } + + /* + Resolve detailed pipeline state timestamps from the GPU buffer to the CPU buffer. + This only seems to be available on macOS 11.0 and later (Big Sur 2020). + */ + private func resolveRenderPassGpuTimestamps(commandBuffer: MTLCommandBuffer, command: mglCommand) { + guard let gpuTimestampBuffer = self.gpuTimestampBuffer, + let cpuTimestampBuffer = self.cpuTimestampBuffer else { + return + } + + if #available(macOS 11.0, *) { + // Explicitly blit recorded timestamps from the GPU's private buffer to the CPU buffer we can read. + // Doing this explicitly with two buffers and a blit seems to be the more reliable of two documented approaches: + // https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/converting_a_gpu_s_counter_data_into_a_readable_format + guard let bltCommandEncoder = commandBuffer.makeBlitCommandEncoder() else { + return + } + bltCommandEncoder.resolveCounters(gpuTimestampBuffer, range: 0..<4, destinationBuffer: cpuTimestampBuffer, destinationOffset: 0) + bltCommandEncoder.endEncoding() + + // The GPU timestamps will be recorded and blited later, while the render pass is executing. + // This callback will run afterwards, when we expect the data to be available. + commandBuffer.addCompletedHandler { [cpuTimestampBuffer, command] commandBuffer in + // Copy each timestamp sample into the given command's timing results. + // This uses the sample buffer array indexes we specified above in setUpRenderPassGpuTimestamps(). + let elementSize = MemoryLayout.size + + // sampleAttachment.startOfVertexSampleIndex = 0 + var vertexStart = MTLCounterResultTimestamp(timestamp: 0) + memcpy(&vertexStart, cpuTimestampBuffer.contents(), elementSize) + command.results.vertexStart = Double(vertexStart.timestamp) + + // sampleAttachment.endOfVertexSampleIndex = 1 + var vertexEnd = MTLCounterResultTimestamp(timestamp: 0) + memcpy(&vertexEnd, cpuTimestampBuffer.contents().advanced(by: elementSize), elementSize) + command.results.vertexEnd = Double(vertexEnd.timestamp) + + // sampleAttachment.startOfFragmentSampleIndex = 2 + var fragmentStart = MTLCounterResultTimestamp(timestamp: 0) + memcpy(&fragmentStart, cpuTimestampBuffer.contents().advanced(by: 2 * elementSize), elementSize) + command.results.fragmentStart = Double(fragmentStart.timestamp) + + // sampleAttachment.endOfFragmentSampleIndex = 3 + var fragmentEnd = MTLCounterResultTimestamp(timestamp: 0) + memcpy(&fragmentEnd, cpuTimestampBuffer.contents().advanced(by: 3 * elementSize), elementSize) + command.results.fragmentEnd = Double(fragmentEnd.timestamp) + } + } + } +} + +/* + Check whether the GPU device supports timestamp stampling at pipeline stage boundaries. + I.e., can we gather start and end of vertex and fragment pipeline stages? + This only seems to be available on macOS 11.0 and later (Big Sur 2020). + */ +private func gpuSupportsStageBoundaryTimestamps(logger: mglLogger, device: MTLDevice) -> Bool { + if #available(macOS 11.0, *) { + // Report all the supported boundaries. + let allBoundaries: [MTLCounterSamplingPoint: String] = [.atStageBoundary: "atStageBoundary", + .atDrawBoundary: "atDrawBoundary", + .atBlitBoundary: "atBlitBoundary", + .atDispatchBoundary: "atDispatchBoundary", + .atTileDispatchBoundary: "atTileDispatchBoundary"] + for (boundary, name) in allBoundaries { + let boundarySupported = device.supportsCounterSampling(boundary) + if boundarySupported { + logger.info(component: "mglRenderer2", + details: "GPU device \"\(device.name)\" supports sampling at boundary \(name).") + } else { + logger.info(component: "mglRenderer2", + details: "GPU device \"\(device.name)\" does not support sampling at boundary \(name).") + } + } + + // Check the one we're specifically interested in. + return device.supportsCounterSampling(.atStageBoundary) + } else { + logger.info(component: "mglRenderer2", + details: "GPU processing stage timestamps are only available on macOS 11.0 or later.") + return false + } +} + +/* + Check which counter sets and counters the GPU device supports. + If the timestamp counter is available, return its counter set so we can use it to gather detailed frame timing. + */ +private func getGpuTimestampCounter(logger: mglLogger, device: MTLDevice) -> MTLCounterSet? { + guard let counterSets = device.counterSets else { + logger.info(component: "mglRenderer2", + details: "GPU device \"\(device.name)\" doesn't support any counter sets.") + return nil + } + + var timestampCounterSet: MTLCounterSet? = nil + for counterSet in counterSets { + for counter in counterSet.counters { + logger.info(component: "mglRenderer2", + details: "GPU device \"\(device.name)\" supports counter set \"\(counterSet.name)\" with counter \"\(counter.name)\".") + + if counterSet.name == MTLCommonCounterSet.timestamp.rawValue && counter.name == MTLCommonCounter.timestamp.rawValue { + timestampCounterSet = counterSet + } + } + } + return timestampCounterSet +} + +/* + Create a new GPU buffer where we can store detailed pipline stage timestamps. + The buffer will be "private" for GPU access only. + The buffer will store 4 timestamps: vertex stage start and end, plus fragment stage start and end. + */ +private func makeGpuTimestampBuffer(logger: mglLogger, device: MTLDevice, counterSet: MTLCounterSet) -> MTLCounterSampleBuffer? { + let descriptor = MTLCounterSampleBufferDescriptor() + descriptor.counterSet = counterSet + descriptor.storageMode = .private + descriptor.sampleCount = 4 + guard let buffer = try? device.makeCounterSampleBuffer(descriptor: descriptor) else { + logger.error(component: "mglRenderer2", details: "Device failed to create a GPU counter sample buffer.") + return nil + } + return buffer +} + +/* + Create a new CPU buffer where we can read out detailed pipline stage timestamps. + The buffer will be "shared" for GPU as well as CPU access. + The buffer will store 4 timestamps: vertex stage start and end, plus fragment stage start and end. + */ +private func makeCpuTimestampBuffer(logger: mglLogger, device: MTLDevice) -> MTLBuffer? { + let counterBufferLength = MemoryLayout.size * 4 + guard let buffer = device.makeBuffer(length: counterBufferLength, options: .storageModeShared) else { + logger.error(component: "mglRenderer2", details: "Device failed to create a CPU counter sample buffer.") + return nil + } + return buffer +} diff --git a/metal/mglMetal/mglServer.swift b/metal/mglMetal/mglServer.swift index 088f6de6..ed78742a 100644 --- a/metal/mglMetal/mglServer.swift +++ b/metal/mglMetal/mglServer.swift @@ -29,8 +29,9 @@ protocol mglServer { // Accept a client connection if not already accepted, return immediately either way. func acceptClientConnection() -> Bool - // Check if data has arrived from the client, return immediately either way. + // Check if data has arrived from the client, return quickly either way. func dataWaiting() -> Bool + func dataWaiting(timeout: Int32) -> Bool // Read data from the client. // Block until all of the expected bytes have arrived, or an error. diff --git a/metal/mglMetal/mglViewController.swift b/metal/mglMetal/mglViewController.swift index 2e88d531..79926ca6 100644 --- a/metal/mglMetal/mglViewController.swift +++ b/metal/mglMetal/mglViewController.swift @@ -18,9 +18,38 @@ import MetalKit // ViewController //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ class ViewController: NSViewController { - // holds our renderer class which does all the work - var renderer : mglRenderer? - + // A common logging interface for app components to share. + // This handles some macOS version dependency, and remembers the app's last error message. + let logger = getMglLogger() + + // holds our renderer class which does the main work, initialized during viewDidLoad() + var renderer: mglRenderer2? + + // Open a connection to the client (eg Matlab) + // based on a default connection address or an address passed as a command line option: + // mglMetal ... -mglConnectionAddress my-address + func commandInterfaceFromCliArgs() -> mglCommandInterface { + // Get the connection address to use from the command line + let arguments = CommandLine.arguments + let optionIndex = arguments.firstIndex(of: "-mglConnectionAddress") ?? -2 + if optionIndex < 0 { + logger.info(component: "ViewController", details: "No command line option passed for -mglConnectionAddress, using a default address.") + } + let address = arguments.indices.contains(optionIndex + 1) ? arguments[optionIndex + 1] : "mglMetal.socket" + logger.info(component: "ViewController", details: "Using connection addresss \(address)") + + // In the future we might inspect the address to decide what kind of server to create, + // like local socket vs internet socket, vs shared memory, etc. + // For now, we always interpret the address as a file system path for a local socket. + let server = mglLocalServer(logger: logger, pathToBind: address) + return mglCommandInterface(logger: logger, server: server) + } + + // This is called normally from viewDidLoad(), or during testing. + func setUpRenderer(view: MTKView, commandInterface: mglCommandInterface) { + renderer = mglRenderer2(logger: logger, metalView: view, commandInterface: commandInterface) + } + //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ // viewDidLoad //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -34,8 +63,8 @@ class ViewController: NSViewController { // Initialize our renderer - this is the function // that handles drawing and where all the action is. - renderer = mglRenderer(metalView: metalView) - + let commandInterface = commandInterfaceFromCliArgs() + setUpRenderer(view: metalView, commandInterface: commandInterface) } //\/\/\/\/\/\/\/\/\/\/\/\/\/\/ @@ -53,7 +82,5 @@ class ViewController: NSViewController { // Update the view, if already loaded. } } - - } diff --git a/metal/mglMetalTests/mglLocalClientServerTests.swift b/metal/mglMetalTests/mglLocalClientServerTests.swift index a3091547..3b48dbf3 100644 --- a/metal/mglMetalTests/mglLocalClientServerTests.swift +++ b/metal/mglMetalTests/mglLocalClientServerTests.swift @@ -10,13 +10,17 @@ import XCTest @testable import mglMetal class mglLocalSocketTests: XCTestCase { + private let logger = getMglLogger() + private let socketPath = "test" - // Server and client are created for each test method. - static let socketPath = "test" - let server = mglLocalServer(pathToBind: socketPath) - let client = mglLocalClient(pathToConnect: socketPath) + private var server: mglLocalServer! + private var client: mglLocalClient! override func setUpWithError() throws { + // Server and client are (re)created for each test method. + server = mglLocalServer(logger: logger, pathToBind: socketPath) + client = mglLocalClient(logger: logger, pathToConnect: socketPath) + // Sanity check server status before connection accepted. XCTAssertGreaterThanOrEqual(server.boundSocketDescriptor, 0) XCTAssertFalse(server.clientIsAccepted()) @@ -78,5 +82,4 @@ class mglLocalSocketTests: XCTestCase { XCTAssertTrue(bytesSentFromServer.elementsEqual(clientBuffer)) XCTAssertFalse(client.dataWaiting()) } - } diff --git a/metal/mglMetalTests/mglMetalTests.swift b/metal/mglMetalTests/mglMetalTests.swift index d455c041..c36e596f 100644 --- a/metal/mglMetalTests/mglMetalTests.swift +++ b/metal/mglMetalTests/mglMetalTests.swift @@ -7,28 +7,397 @@ // import XCTest +import MetalKit @testable import mglMetal +// Helpful post on setting up this kind of test: http://fullytyped.com/2019/01/07/on-screen-unit-tests/ class mglMetalTests: XCTestCase { + private let testAddress = "mglMetalTests.socket" + + private var server: mglLocalServer! + private var client: mglLocalClient! + + private var viewController: ViewController! + private var view: MTKView! + private var commandInterface: mglCommandInterface! + + private var offscreenTexture: MTLTexture! override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. + // XCTestCase automatically launches our whole app and runs these tests in the same process. + // This means we can drill down and find our custom view controller, configure it, and control it. + let window = NSApplication.shared.mainWindow! + viewController = window.contentViewController as? ViewController + + // Tests will frames one at a time instead of using a scheduled frame rate. + view = viewController.view as? MTKView + view.isPaused = true + view.enableSetNeedsDisplay = false + + // Create a client-server pair we can use to test-drive the app. + server = mglLocalServer(logger: viewController.logger, pathToBind: testAddress) + client = mglLocalClient(logger: viewController.logger, pathToConnect: testAddress) + + // Use a private server and connection address during testing instead of the default. + commandInterface = mglCommandInterface(logger: viewController.logger, server: server) + viewController.setUpRenderer(view: view, commandInterface: commandInterface) + + // Trigger one frame to allow the server to accept the test client's connection. + view.draw() + let accepted = server.clientIsAccepted() + XCTAssertTrue(accepted) + + let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: .rgba32Float, + width: 640, + height: 480, + mipmapped: false + ) + textureDescriptor.usage = [.renderTarget, .shaderRead, .shaderWrite] + offscreenTexture = view.device!.makeTexture(descriptor: textureDescriptor) } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. + client.disconnect() + server.disconnect() + } + + private func drawNextFrame(sleepSecs: TimeInterval = TimeInterval.zero) { + view.draw() + if (sleepSecs > TimeInterval.zero) { + Thread.sleep(forTimeInterval: sleepSecs) + } } - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. + func assertTimestampReply(atLeast: Double = 0.0) { + var timestamp = 0.0 + let bytesRead = client.readData(buffer: ×tamp, expectedByteCount: 8) + XCTAssertEqual(bytesRead, 8) + XCTAssertGreaterThanOrEqual(timestamp, atLeast) } - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. + func assertCommandResultsReply( + commandCode: mglCommandCode, + status: UInt32 = 1, + processedAtLeast: Double = 0.0, + timestampAtLeast: Double = 0.0 + ) { + assertCommandCodeReply(expected: commandCode) + assertUInt32Reply(expected: status) + assertTimestampReply(atLeast: processedAtLeast) + assertTimestampReply(atLeast: timestampAtLeast) + assertTimestampReply(atLeast: timestampAtLeast) + assertTimestampReply(atLeast: timestampAtLeast) + assertTimestampReply(atLeast: timestampAtLeast) + assertTimestampReply(atLeast: timestampAtLeast) + assertTimestampReply(atLeast: timestampAtLeast) + } + + func assertCommandCodeReply(expected: mglCommandCode) { + var commandCode = mglUnknownCommand + let bytesRead = client.readData(buffer: &commandCode, expectedByteCount: 2) + XCTAssertEqual(bytesRead, 2) + XCTAssertEqual(commandCode, expected) + } + + func assertUInt32Reply(expected: UInt32) { + var value = UInt32(0) + let bytesRead = client.readData(buffer: &value, expectedByteCount: 4) + XCTAssertEqual(bytesRead, 4) + XCTAssertEqual(value, expected) + } + + private func sendCommandCode(commandCode: mglCommandCode) { + var codeValue = commandCode.rawValue + let bytesSent = client.sendData(buffer: &codeValue, byteCount: 2) + XCTAssertEqual(bytesSent, 2) + } + + private func sendColor(r: Float32, g: Float32, b: Float32) { + let color: [Float32] = [r, g, b] + let bytesSent = color.withUnsafeBufferPointer { + client.sendData(buffer: $0.baseAddress!, byteCount: 12) } + XCTAssertEqual(bytesSent, 12) } + private func sendUInt32(value: UInt32) { + var value2 = value + let bytesSent = client.sendData(buffer: &value2, byteCount: 4) + XCTAssertEqual(bytesSent, 4) + } + + private func sendXYZRGBVertex(x: Float32, y: Float32, z: Float32, r: Float32, g: Float32, b: Float32) { + let vertex: [Float32] = [x, y, z, r, g, b] + let bytesSent = vertex.withUnsafeBufferPointer { + client.sendData(buffer: $0.baseAddress!, byteCount: 24) + } + XCTAssertEqual(bytesSent, 24) + } + + func assertViewClearColor(r: Double, g: Double, b: Double) { + XCTAssertEqual(view.clearColor.red, r) + XCTAssertEqual(view.clearColor.green, g) + XCTAssertEqual(view.clearColor.blue, b) + XCTAssertEqual(view.clearColor.alpha, 1.0) + } + + func testClearColorViaClientBytes() { + // Send a clear command with the color green. + sendCommandCode(commandCode: mglSetClearColor) + sendColor(r: 0.0, g: 1.0, b: 0.0) + + // Send a flush command to present the new clear color. + sendCommandCode(commandCode: mglFlush) + + // Consume the clear and flush commands and present a frame for visual inspection. + drawNextFrame(sleepSecs: 0.5) + + // Processing the clear command should have set the view's clear color for future frames. + assertViewClearColor(r: 0.0, g: 1.0, b: 0.0) + + // The server should send an ack timestamp and results record for the clear color command. + assertTimestampReply() + assertCommandResultsReply(commandCode: mglSetClearColor) + + // It should send the ack timestamp for the flush command as well. + assertTimestampReply() + XCTAssertFalse(client.dataWaiting()) + + // The last result record, for flush, waits until the start of the next frame. + drawNextFrame() + assertCommandResultsReply(commandCode: mglFlush) + XCTAssertFalse(client.dataWaiting()) + } + + private func assertSuccess(command: mglCommand, timestampAtLeast: Double = 0.0) { + XCTAssertTrue(command.results.success) + XCTAssertGreaterThanOrEqual(command.results.ackTime, timestampAtLeast) + XCTAssertGreaterThanOrEqual(command.results.processedTime, timestampAtLeast) + XCTAssertGreaterThanOrEqual(command.results.vertexStart, timestampAtLeast) + XCTAssertGreaterThanOrEqual(command.results.vertexEnd, timestampAtLeast) + XCTAssertGreaterThanOrEqual(command.results.fragmentStart, timestampAtLeast) + XCTAssertGreaterThanOrEqual(command.results.fragmentEnd, timestampAtLeast) + XCTAssertGreaterThanOrEqual(command.results.drawableAcquired, timestampAtLeast) + XCTAssertGreaterThanOrEqual(command.results.drawablePresented, timestampAtLeast) + } + + private struct RGBAFloat32Pixel : Equatable { + let r: Float32 + let g: Float32 + let b: Float32 + let a: Float32 + } + + private func assertAllOffscreenPixels(expectedPixel: RGBAFloat32Pixel) { + let region = MTLRegionMake2D(0, 0, offscreenTexture.width, offscreenTexture.height) + let bytesPerRow = offscreenTexture.width * MemoryLayout.stride + let pixelPointer = UnsafeMutablePointer.allocate(capacity: offscreenTexture.width * offscreenTexture.height) + offscreenTexture.getBytes(pixelPointer, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0) + let pixelBuffer = UnsafeBufferPointer(start: pixelPointer, count: offscreenTexture.width * offscreenTexture.height) + let allPixelsAsExpected = pixelBuffer.allSatisfy { pixel in + pixel == expectedPixel + } + XCTAssertTrue(allPixelsAsExpected) + } + + func testClearColorInMemory() { + // Create a texture for offscreen rendering. + let createTexture = mglCreateTextureCommand(texture: offscreenTexture) + commandInterface.addLast(command: createTexture) + drawNextFrame() + assertSuccess(command: createTexture) + XCTAssertGreaterThan(createTexture.textureNumber, 0) + + // Asssign the new texture as the offscreen rendering target. + let setRenderTarget = mglSetRenderTargetCommand(textureNumber: createTexture.textureNumber) + commandInterface.addLast(command: setRenderTarget) + drawNextFrame() + assertSuccess(command: setRenderTarget) + + // Enqueue clear and flush commands. + let clear = mglSetClearColorCommand(red: 0.0, green: 0.0, blue: 1.0) + commandInterface.addLast(command: clear) + let flush = mglFlushCommand() + commandInterface.addLast(command: flush) + + // Processing the clear command should change the view clear color. + drawNextFrame() + assertSuccess(command: clear) + assertViewClearColor(r: 0.0, g: 0.0, b: 1.0) + + // The the processed time for the flush command waits until the start of the next frame. + drawNextFrame() + assertSuccess(command: flush) + + // Presenting the frame command should draw the clear color to all pixels. + let expectedPixel = RGBAFloat32Pixel(r: 0.0, g: 0.0, b: 1.0, a: 1.0) + assertAllOffscreenPixels(expectedPixel: expectedPixel) + } + + func testCommandBatchViaClientBytes() { + // The command interface should start out in its "none" state, with no batch happening. + XCTAssertEqual(commandInterface.getBatchState(), BatchState.none) + + // Put the command interface into batch "building" state. + sendCommandCode(commandCode: mglStartBatch) + drawNextFrame() + assertTimestampReply() + XCTAssertEqual(commandInterface.getBatchState(), BatchState.building) + + // Send several pairs of set-clear-color and flush commands. + // Each one should get back an ack time and a placeholder results record. + // Red + sendCommandCode(commandCode: mglSetClearColor) + sendColor(r: 1.0, g: 0.0, b: 0.0) + drawNextFrame() + assertTimestampReply() + assertCommandResultsReply(commandCode: mglSetClearColor) + + sendCommandCode(commandCode: mglFlush) + drawNextFrame() + assertTimestampReply() + assertCommandResultsReply(commandCode: mglFlush) + + // Green + sendCommandCode(commandCode: mglSetClearColor) + sendColor(r: 0.0, g: 1.0, b: 0.0) + drawNextFrame() + assertTimestampReply() + assertCommandResultsReply(commandCode: mglSetClearColor) + + sendCommandCode(commandCode: mglFlush) + drawNextFrame() + assertTimestampReply() + assertCommandResultsReply(commandCode: mglFlush) + + // Blue + sendCommandCode(commandCode: mglSetClearColor) + sendColor(r: 0.0, g: 0.0, b: 1.0) + drawNextFrame() + assertTimestampReply() + assertCommandResultsReply(commandCode: mglSetClearColor) + + sendCommandCode(commandCode: mglFlush) + drawNextFrame() + assertTimestampReply() + assertCommandResultsReply(commandCode: mglFlush) + + // These commands should all be enqueued as todo, and not yet processed or reported. + XCTAssertFalse(client.dataWaiting()) + + // Since the commands aren't processed yet, the view's clear color should be default gray. + assertViewClearColor(r: 0.5, g: 0.5, b: 0.5) + + // Put the command interface into batch "processing" state. + sendCommandCode(commandCode: mglProcessBatch) + drawNextFrame(sleepSecs: 0.5) + assertTimestampReply() + XCTAssertEqual(commandInterface.getBatchState(), BatchState.processing) + + // We should now see the clear colors in order: red, green, blue. + // The socket should remain quiet during processing. + // Red + assertViewClearColor(r: 1.0, g: 0.0, b: 0.0) + XCTAssertFalse(client.dataWaiting()) + + // Green + drawNextFrame(sleepSecs: 0.5) + assertViewClearColor(r: 0.0, g: 1.0, b: 0.0) + XCTAssertFalse(client.dataWaiting()) + + // Blue + drawNextFrame(sleepSecs: 0.5) + assertViewClearColor(r: 0.0, g: 0.0, b: 1.0) + XCTAssertFalse(client.dataWaiting()) + + // Wait for the signal that tells us all commands are processed. + // This also tells us how many result records are pending, in this case 6. + drawNextFrame() + XCTAssertTrue(client.dataWaiting()) + assertUInt32Reply(expected: 6) + + // The command responses should only be pending, not sent yet. + XCTAssertFalse(client.dataWaiting()) + + // Put the command interface back into its normal "none" state. + sendCommandCode(commandCode: mglFinishBatch) + drawNextFrame() + assertTimestampReply() + XCTAssertEqual(commandInterface.getBatchState(), BatchState.none) + + // Expect a batch of results records. + // These arrive in "vectorized" order, one field at a time across all records. + // Command code. + assertCommandCodeReply(expected: mglSetClearColor) + assertCommandCodeReply(expected: mglFlush) + assertCommandCodeReply(expected: mglSetClearColor) + assertCommandCodeReply(expected: mglFlush) + assertCommandCodeReply(expected: mglSetClearColor) + assertCommandCodeReply(expected: mglFlush) + + // Status. + for _ in 0..<6 { assertUInt32Reply(expected: 1) } + + // Processed Time + for _ in 0..<6 { assertTimestampReply() } + + // Vertex start. + for _ in 0..<6 { assertTimestampReply() } + + // Vertex end. + for _ in 0..<6 { assertTimestampReply() } + + // Fragment start. + for _ in 0..<6 { assertTimestampReply() } + + // Fragment end. + for _ in 0..<6 { assertTimestampReply() } + + // Drawable acquired. + for _ in 0..<6 { assertTimestampReply() } + + // Drawable presented. + for _ in 0..<6 { assertTimestampReply() } + + XCTAssertFalse(client.dataWaiting()) + } + + func testPolygonViaClientBytes() { + // Send a clear command with the color gray. + sendCommandCode(commandCode: mglSetClearColor) + sendColor(r: 0.25, g: 0.25, b: 0.25) + + // Send some rainbow polygon vertex data as [xyz rgb]. + sendCommandCode(commandCode: mglPolygon) + sendUInt32(value: 5) + sendXYZRGBVertex(x: -0.3, y: -0.4, z: 0.0, r: 1.0, g: 0.0, b: 0.0) + sendXYZRGBVertex(x: -0.6, y: 0.1, z: 0.0, r: 1.0, g: 0.0, b: 0.0) + sendXYZRGBVertex(x: 0.4, y: -0.2, z: 0.0, r: 0.0, g: 1.0, b: 0.0) + sendXYZRGBVertex(x: -0.5, y: 0.5, z: 0.0, r: 0.0, g: 1.0, b: 1.0) + sendXYZRGBVertex(x: 0.5, y: 0.3, z: 0.0, r: 0.0, g: 0.0, b: 1.0) + + // Send a flush command to present the clear color and polygon. + sendCommandCode(commandCode: mglFlush) + + // Consume the commands and present a frame for visual inspection. + drawNextFrame(sleepSecs: 0.5) + + // The server should send an ack timestamp and results record for each command. + assertTimestampReply() + assertCommandResultsReply(commandCode: mglSetClearColor) + + assertTimestampReply() + assertCommandResultsReply(commandCode: mglPolygon) + + assertTimestampReply() + + // The server should send a results record for the clear color and polygon commands. + XCTAssertFalse(client.dataWaiting()) + + // The last result record, for flush, waits until the start of the next frame. + drawNextFrame() + assertCommandResultsReply(commandCode: mglFlush) + XCTAssertFalse(client.dataWaiting()) + } } diff --git a/metal/readme.md b/metal/readme.md new file mode 100644 index 00000000..55ed27bd --- /dev/null +++ b/metal/readme.md @@ -0,0 +1,147 @@ +# Mgl Metal App and Commands + +This readme describes the design of the Mgl Metal App, with a focus on commands. +Commands are objects that the app is able to receive, enqueue, process / render, and report on. + +Here's a visual overview. + +![Overview of Mgl Metal app command model](command-model-etc.png) + +## Components + +Here are descriptions of the components involved in processing Mgl Metal commands. + +### client (Matlab) + +The client is a separate application (Matlab) which connectes to the Mgl Metal app via a socket. +The client controls Mgl Metal by sending commands as formatted bytes. +It receives status and timing results back Mgl Metal as formatted bytes. + +During development, Mgl Metal can also be driven by XCode tests. +These can drive Mgl Metal with formatted bytes, like the Matlab client, or by passing and inspecting command objects directly in memory. + +### command interface + +The Mgl Metal [command interface](mglMetal/mglCommandInterface.swift) uses a [socket server](mglMetal/mglServer.swift) to receive formatted bytes from the client. +Based on specific [command codes](mglMetal/mglCommandTypes.h) received, it instantiates command objects and adds these to its **todo** queue for processing. + +The command interface also takes processed commands from its **done** queue and writes timing and other results to the socket for the client to read. + +The command interface doesn't know the details of any particular command, or how these get processed / rendered. +Instead, it lets the renderer component (below) do the processing. It exposes the **todo** and **done** queues to the renderer via its `next()` and `done()` methods. + +### renderer + +The Mgl Metal [renderer](mglMetal/mglRenderer2.swift) takes one command at a time from the command interface (above) and processes it. Processing a command can include getting or setting the state of the Mgl Metal app itself, and/or rendering graphics. + +The renderer manages short-lived system resources for each frame including: + + - the system's [drawable](https://developer.apple.com/documentation/quartzcore/cametaldrawable) for displaying a frame of graphics + - a [render pass](https://developer.apple.com/documentation/metal/render_passes) for instructing the GPU what to render + +The renderer also manages its own long-lived state including: + + - [depth and stencil state](mglMetal/mglDepthStencilConfig.swift) for stencils being created or applied + - [color rendering state](mglMetal/mglColorRenderingConfig.swift) for the onscreen pixel format, custom textures, and onscreen vs offscreen rendering targets + - the `deg2metal` coordinate transform matrix applied to rendered scenes + - the system's GPU device + +The renderer doesn't know the details of any particular command or how these get created. Instead, it takes each command from the command interface using `next()`, processes it, and returns it to the command interface using `done()`. During processing it calls standard methods on each command, at the appropriate times: `doNondrawingWork()` and `draw()`. These methods are declared in the Mgl Metal command model (below) which provides a consistent interface over various command-specific behaviors. + +### command model + +The Mgl Metal [command model](mglMetal/mglCommandModel.swift) is an abstraction that covers all commands, allowing them to fit a common shape expected by the command interface and the renderer. Within this model, each command is free to manage its own details. Mgl Metal has many specific command [implementations](https://github.com/justingardner/mgl/tree/commandModel/metal/mglMetal/commands) for different tasks. + +Once a command object is read from the socket and initialized it is considered ready for processing, either immediately or at a later time. This means each command must store its own parameters and data. usually this means reading command-specific values from the command interface as part of a command's `init?()` method. Some commands will also write data to GPU buffers during `init()?`, and store references the buffers. + +During processing, the renderer will call two standard methods of each command: + - `doNondrawingWork()` is a chance for the command to get or set the state of the Mgl Metal app. It can also store any command-specific results it wants to return to the client. + - `draw()` is a chance to encode Metal rendering commands as part of a render pass. + +After processing, the command interface will call one other standard method of each command: + - `writeQueryResults()` is a chance for the command to write any command-specific results it wants to return to the client. + +## Lifecycle of a Command + +Here's sequential walkthrough of how a command moves through the app. + +### 1. initialized from client bytes + +A command starts when a connected client sends a [command code](mglMetal/mglCommandTypes.h) to the socket server. +The command interface reads this code and sends back an **ack** timestamp to the client. +Based on the specific command code the command interface chooses a command implementation and calls its `init?()` method, to obtain a new command object. + +Here are some examples of command initialization and `init?()` implementations: + + - [mglFlushCommand](mglMetal/commands/mglFlushCommand.swift) requires no parameters or other data and initializes unconditionally. + - [mglSetClearColorCommand](mglMetal/commands/mglSetClearColorCommand.swift) requires a color paramter. It attempts to read this color from the command interface and stores the result in one of its own fields. + - [mglDotsCommand](mglMetal/commands/mglDotsCommand.swift) requires vertex data. It attempts to read a vertex array from the command interface and stores the result directly to a GPU device buffer. + +Once initialized commands are considered complete and ready for processing, either immediately or at a later time. + +### 2. added to **todo** + +The command interface adds initialized commands to its **todo** queue. +From here they are processed by the renderer in the order they arrived. +This usually happens right away, before the **todo** queue has a chance to grow. + +The command interface also supports command batches, where the **todo** queue is allowed to fill up with multiple fully initialized commands, before allowing the renderer to start processing them. + +### 3. processed + +The renderer will process each next available command from the command interface's **todo** queue. +This happens periodically when the system invokes the renderer's `draw()` method. + +Processing starts with a call to each command's `doNondrawingWork()`. +This is a chance for the command to get or set the state of the Mgl Metal app and remember any command-specific results it wants to return to the client. + +Here are some examples of non-drawing work: + + - [mglSetClearColorCommand](mglMetal/commands/mglSetClearColorCommand.swift) updates the clear color of the app's Metal View, to be applied to the next rendering pass. + - [mglCreateTextureCommand](mglMetal/commands/mglCreateTextureCommand.swift) initializes a new Metal texture and remembers its new texture number, to return to the client. + - [mglDotsCommand](mglMetal/commands/mglDotsCommand.swift) uses the default no-op, since it's only interested in drawing (below). + +Some commands will be complete here. Others will also want to draw graphics. When the renderer gets a drawing command it will set up a Metal render pass and enter a tight loop to receive additional commands that should `draw()` into the same frame. Each of these commands will have `doNondrawingWork()` and `draw()` called before being added to the command interface's **done** queue. + +Here are some drawing examples: + + - [mglDotsCommand](mglMetal/commands/mglDotsCommand.swift) encodes Metal commands for the current render pass in order to send its vertex data through the Mgl Metal "dots" shaders. + - [mglSetClearColorCommand](mglMetal/commands/mglSetClearColorCommand.swift) and [mglCreateTextureCommand](mglMetal/commands/mglCreateTextureCommand.swift) use the default no-op `draw()` implementation. These can be used during a rendering tight loop to modify the state of the app, but they won't draw anything. + - [mglFlushCommand](mglMetal/commands/mglFlushCommand.swift) is a special case that tells the app to exit the drawing tight loop, end the rendering pass, and present the current frame. + +### 4. added to **done** + +After processing, the renderer gives each command back to the command interface and its **done** queue. +From here, the command interface will send command-specifc and standard results back to the client, in the same order that the commands arrived. +This usually happens right away, before the **done** queue has a chance to grow. + +The command interface also supports command batches, where the **done** queue is allowed to fill up with multiple completed commands, until the client requests them. This keeps the socket quiet during batch processing. + +### 5. reported as bytes to client + +Each **done** command is reported back to the client including: + - optional, command-specific data + - a standard record of status and timing + +First, the command interface calls a command's `writeQueryResults()` method to send optional, command-specific data. Here are some examples of command-specific query results: + + - [mglCreateTextureCommand](mglMetal/commands/mglCreateTextureCommand.swift) writes the new texture number of its new texture to the command interface. + - [mglReadTextureCommand](mglMetal/commands/mglReadTextureCommand.swift) does a sanity check to see if it has texture image data to report. If not, it writes a negative "heads up" to the client indicating that no image data will follow. Otherwise, it writes a positive "heads up" followed by image data. + - [mglDotsCommand](mglMetal/commands/mglDotsCommand.swift) uses the default-no-op since it has no command-specific results to report to the client. + +Finally, the command interface writes a standard record of staus and timing for each command. These results are gathered automatically by the command interface and the renderer and assigned to commands wherever applicable and available. They include: + + - `commandCode` to echo back to the client + - `success` true or false, wheter the command processed successfully + - `ackTime` CPU time when the command was received + - `processedTime` CPU time when the command was done + - `vertexStart` GPU time when the render pipeline vertex stage began (for flush commands) + - `vertexEnd` GPU time when the render pipeline vertex stage finished (for flush commands) + - `fragmentStart` GPU time when the render pipeline fragment stage began (for flush commands) + - `fragmentEnd` GPU time when the render pipeline fragment stage finished (for flush commands) + - `drawableAcquired` CPU time when Mgl Metal got hold of the system drawable for a frame (for flush and drawing commands) + - `drawablePresented` CPU time when the system presented the drawable for the previous frame (for flush commands) + +The Matlab client expects all these fields to be reported for each command and reads the results with [mglReadCommandResults](../mgllib/mglReadCommandResults.m). + +Once a command has been reported back to the client, it ends and goes away. diff --git a/mgllib/mglBltTexture.m b/mgllib/mglBltTexture.m index d94472ae..b3e3c1c6 100644 --- a/mgllib/mglBltTexture.m +++ b/mgllib/mglBltTexture.m @@ -1,7 +1,7 @@ % mglBltTexture.m % % $Id$ -% usage: [ackTime, processedTime, setupTime] = mglBltTexture(tex, position, hAlignment, vAlignment, rotation, phase, width, height) +% usage: results = mglBltTexture(tex, position, hAlignment, vAlignment, rotation, phase, width, height) % by: justin gardner % date: 05/10/06 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -52,7 +52,7 @@ % imageTex = mglCreateTexture(image1d); % mglBltTexture(imageTex,[0 0 5]); % mglFlush; -function [ackTime, processedTime, setupTime] = mglBltTexture(tex, position, hAlignment, vAlignment, rotation, phase, width, height) +function results = mglBltTexture(tex, position, hAlignment, vAlignment, rotation, phase, width, height) % if nargin < 2, position = [0 0]; end % if nargin < 3, hAlignment = 0; end @@ -131,11 +131,9 @@ height = repmat(height, textureCount, 1); end -ackTime = zeros([1, textureCount]); -processedTime = zeros([1, textureCount]); -setupTime = zeros([1, textureCount]); +resultCell = cell([1, textureCount]); for ii = 1:textureCount - [ackTime(ii), processedTime(ii), setupTime(ii)] = mglMetalBltTexture( ... + resultCell{ii} = mglMetalBltTexture( ... tex(ii), ... position(ii,:), ... hAlignment(ii), ... @@ -145,3 +143,4 @@ width(ii), ... height(ii)); end +results = [resultCell{:}]; diff --git a/mgllib/mglClearScreen.m b/mgllib/mglClearScreen.m index 2a6cf223..7749be29 100644 --- a/mgllib/mglClearScreen.m +++ b/mgllib/mglClearScreen.m @@ -1,7 +1,7 @@ % mglClearScreen.m % % $Id$ -% usage: [ackTime, processedTime, setupTime] = mglClearScreen([clearColor], [clearBits]) +% usage: results = mglClearScreen([clearColor], [clearBits]) % by: Justin Gardner % date: 09/27/2021 % copyright: (c) 2021 Justin Gardner (GPL see mgl/COPYING) @@ -29,7 +29,7 @@ % mglClearScreen([0.7 0.2 0.5]); % mglFlush(); % -function [ackTime, processedTime, setupTime] = mglClearScreen(clearColor, clearBits, socketInfo) +function results = mglClearScreen(clearColor, clearBits, socketInfo) if nargin == 2 disp(sprintf('(mglClearScreen) clearBits not implemented in mgl 3.0')); @@ -55,7 +55,7 @@ clearColor = [clearColor clearColor clearColor]; elseif numel(clearColor) ~= 3 disp(sprintf('(mglClearScreen) Color must be a scalar or an array of length 3 (found len: %i)',length(clearColor))); - ackTime = -mglGetSecs; processedTime = -mglGetSecs; setupTime = -mglGetSecs; + results = []; return end clearColor = clearColor(:); @@ -73,10 +73,10 @@ mglSocketWrite(socketInfo, socketInfo(1).command.mglSetClearColor); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, single(clearColor)); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime, setupTime); % check if processedTime is negative which indicates an error -if processedTime < 0 +if any([results.processedTime] < 0) % display error - mglPrivateDisplayProcessingError(socketInfo, ackTime, processedTime, mfilename); + mglPrivateDisplayProcessingError(socketInfo, results, mfilename); end diff --git a/mgllib/mglContextFlushAll.m b/mgllib/mglContextFlushAll.m index 04149908..cad5d799 100644 --- a/mgllib/mglContextFlushAll.m +++ b/mgllib/mglContextFlushAll.m @@ -1,12 +1,12 @@ % mglContextFlushAll: Flush all stashed and active mgl contexts. % % $Id$ -% usage: [ackTime, processedTime] = mglContextFlushAll() +% usage: results = mglContextFlushAll() % by: ben heasly % date: 09/07/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) % purpose: Flush all stashed and active mgl contexts. -% usage: [ackTime, processedTime] = mglContextFlushAll() +% usage: results = mglContextFlushAll() % % This will flush all known mgl contexts, including the active % context in "global mgl", plus any contexts that were @@ -26,7 +26,7 @@ % % % Clean up all contexts, whether active or stashed. % mglContextCloseAll(); -function [ackTime, processedTime] = mglContextFlushAll() +function results = mglContextFlushAll() % Collect socketInfo for all known contexts, active and/or stashed. % My (BSH) hope is to handle looping within the mglSocket* mex-functions @@ -38,12 +38,11 @@ if isempty(mglStashedContexts) if isempty(mgl) % No active or stashed contexts found, no-op. - ackTime = []; - processedTime = []; + results = []; return; else % No stashed contexts, just flush the active one. - [ackTime, processedTime] = mglFlush(mgl.activeSockets); + results = mglFlush(mgl.activeSockets); return; end else @@ -53,12 +52,12 @@ stashedSocketInfo = cat(2, stashedActiveSockets{:}); if isempty(mgl) % No active context, just flush the stashed ones. - [ackTime, processedTime] = mglFlush(stashedSocketInfo); + results = mglFlush(stashedSocketInfo); return; else % Both active and stashed contexts, flush them all! allSocketInfo = cat(2, mgl.activeSockets, stashedSocketInfo); - [ackTime, processedTime] = mglFlush(allSocketInfo); + results = mglFlush(allSocketInfo); return; end end diff --git a/mgllib/mglConvertCommandResults.m b/mgllib/mglConvertCommandResults.m new file mode 100644 index 00000000..91b6065d --- /dev/null +++ b/mgllib/mglConvertCommandResults.m @@ -0,0 +1,65 @@ +% mglConvertCommandResults: Convert GPU result timestamps to CPU time. +% +% usage: results = mglConvertCommandResults(results, cpuBefore, gpuBefore, cpuAfter, gpuAfter) +% by: Benjamin Heasly +% date: 01/30/2024 +% copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) +% purpose: Convert GPU results timestamps to CPU time +% usage: results = mglConvertCommandResults(results, cpuBefore, gpuBefore, cpuAfter, gpuAfter) +% +% This function aligns GPU timestamps to CPU time, following the +% interpolation strategy discussed in Apple's Metal docs here: +% https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/converting_gpu_timestamps_into_cpu_time +% +% Inputs: +% +% results: struct array with command results, as from +% mglReadCommandResults() +% cpuBefore: a CPU timestamp preceeding results, as from +% mglMetalSampleTimestamps() +% gpuBefore: a GPU timestamp preceeding results, as from +% mglMetalSampleTimestamps() +% cpuAfter: a CPU timestamp following results, as from +% mglMetalSampleTimestamps() +% gpuAfter: a GPU timestamp following results, as from +% mglMetalSampleTimestamps() +% +% Output: +% +% results: the same results struct give, with GPU timestamps converted +% to values comparable to CPU timestamps and mglGetSecs(). +% +% Example: +% +% mglOpen() +% [cpuBefore, gpuBefore] = mglMetalSampleTimestamps(); +% results = mglFlush() +% [cpuAfter, gpuAfter] = mglMetalSampleTimestamps(); +% results2 = mglConvertCommandResults(results, cpuBefore, gpuBefore, cpuAfter, gpuAfter) +function results = mglConvertCommandResults(results, cpuBefore, gpuBefore, cpuAfter, gpuAfter) + +% Convert specific timestamps from GPU to CPU time. +% We know which ones to convert because of implementation details, +% not because there's anything special we can tell about them from here. +[results.vertexStart] = num2list(gpu2cpu([results.vertexStart], cpuBefore, gpuBefore, cpuAfter, gpuAfter)); +[results.vertexEnd] = num2list(gpu2cpu([results.vertexEnd], cpuBefore, gpuBefore, cpuAfter, gpuAfter)); +[results.fragmentStart] = num2list(gpu2cpu([results.fragmentStart], cpuBefore, gpuBefore, cpuAfter, gpuAfter)); +[results.fragmentEnd] = num2list(gpu2cpu([results.fragmentEnd], cpuBefore, gpuBefore, cpuAfter, gpuAfter)); + +function cpu = gpu2cpu(gpu, cpuBefore, gpuBefore, cpuAfter, gpuAfter) +% Leave zeros alone, since these indicate no data. +if all(gpu == 0) + cpu = gpu; + return +end + +% Express gpu as a fraction of the range [gpuBefore gpuAfter]. +gpuRange = gpuAfter - gpuBefore; +gpuFraction = (gpu - gpuBefore) / gpuRange; + +% Compute cpu from the same fraction, of the range [cpuBefore, cpuAfter]. +cpuRange = cpuAfter - cpuBefore; +cpu = cpuBefore + gpuFraction * cpuRange; + +function varargout = num2list(numbers) +varargout = num2cell(numbers); diff --git a/mgllib/mglCreateTexture.m b/mgllib/mglCreateTexture.m index 483035ee..ec77c938 100644 --- a/mgllib/mglCreateTexture.m +++ b/mgllib/mglCreateTexture.m @@ -1,7 +1,7 @@ % mglCreateTexture.m % % $Id$ -% usage: [texture, ackTime, processedTime] = mglCreateTexture(image) +% usage: [texture, results] = mglCreateTexture(image) % by: justin gardner % date: 04/10/06 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -43,7 +43,7 @@ % mglBltTexture(tex,[mglGetParam('screenWidth')/2 mglGetParam('screenHeight')/2]); % mglFlush; % -function [texture, ackTime, processedTime] = mglCreateTexture(image, axes, liveBuffer, textureParams, socketInfo) +function [texture, results] = mglCreateTexture(image, axes, liveBuffer, textureParams, socketInfo) persistent warnOnce if isempty(warnOnce) warnOnce = true; end @@ -87,10 +87,10 @@ image = cat(3, image, ones(size(image,1:2))); end -[texture, ackTime, processedTime] = mglMetalCreateTexture(image, [], [], [], socketInfo); +[texture, results] = mglMetalCreateTexture(image, [], [], [], socketInfo); % check if processedTime is negative which indicates an error -if any(processedTime < 0) +if any([results.processedTime] < 0) % display error - mglPrivateDisplayProcessingError(socketInfo, ackTime, processedTime, mfilename); + mglPrivateDisplayProcessingError(socketInfo, results, mfilename); end diff --git a/mgllib/mglDeleteTexture.m b/mgllib/mglDeleteTexture.m index cb8294ca..9094e2f6 100644 --- a/mgllib/mglDeleteTexture.m +++ b/mgllib/mglDeleteTexture.m @@ -20,7 +20,7 @@ % mglBltTexture(texture,[0 0]); % mglFlush; % mglDeleteTexture(texture); -function [texture, ackTime, processedTime] = mglDeleteTexture(texture, socketInfo) +function [texture, results] = mglDeleteTexture(texture, socketInfo) if nargin < 1 help mglDeleteTexture @@ -34,16 +34,18 @@ if isfield(texture,'textureNumber') % if texture is an array then we delete each element separately + resultCell = cell(size(texture)); for i = 1:length(texture) - [ackTime, processedTime] = deleteTexture(texture(i).textureNumber, socketInfo); + resultCell{i} = deleteTexture(texture(i).textureNumber, socketInfo); end + results = [resultCell{:}]; texture(i).textureNumber = -1; else disp('(mglDeleteTexture) Input is not a texture'); end -function [ackTime, processedTime] = deleteTexture(textureNumber, socketInfo) +function results = deleteTexture(textureNumber, socketInfo) mglSocketWrite(socketInfo, socketInfo(1).command.mglDeleteTexture); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, textureNumber); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglDisplayCursor.m b/mgllib/mglDisplayCursor.m index d657ed9e..dc117690 100644 --- a/mgllib/mglDisplayCursor.m +++ b/mgllib/mglDisplayCursor.m @@ -13,11 +13,10 @@ % %mglOpen(); %mglDisplayCursor(1); -function [ackTime, processedTime] = mglDisplayCursor(dispCursor) +function results = mglDisplayCursor(dispCursor) % default values for return variables -ackTime = []; -processedTime = []; +results = []; % get socket global mgl; @@ -35,4 +34,4 @@ end % get processed time -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglFillOval.m b/mgllib/mglFillOval.m index 6d5b6089..e39eb624 100644 --- a/mgllib/mglFillOval.m +++ b/mgllib/mglFillOval.m @@ -1,7 +1,7 @@ % mglFillOval - draw filled oval(s) on the screen % % $Id$ -% usage: [ackTime, processedTime] = mglFillOval(x, y, size, color, antialiasing) +% usage: results = mglFillOval(x, y, size, color, antialiasing) % by: Benjamin Heasly % date: 03-17.2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -26,7 +26,7 @@ %mglFillOval(x, y, sz, [1 0 0]); %mglFlush(); % -function [ackTime, processedTime] = mglFillOval(x, y, size, color, antialiasing) +function results = mglFillOval(x, y, size, color, antialiasing) nArcs = numel(x); if ~isequal(numel(y), nArcs) @@ -72,4 +72,4 @@ border = zeros(1, nArcs, 'single'); border(:) = antialiasing; -[ackTime, processedTime] = mglMetalArcs(xyz, rgba, radii, wedge, border); \ No newline at end of file +results = mglMetalArcs(xyz, rgba, radii, wedge, border); \ No newline at end of file diff --git a/mgllib/mglFillRect.m b/mgllib/mglFillRect.m index 3c5a5e4e..8736c9ae 100644 --- a/mgllib/mglFillRect.m +++ b/mgllib/mglFillRect.m @@ -1,7 +1,7 @@ % mglFillRect - draw filled rectangle(s) on the screen % % $Id$ -% usage: [ackTime, processedTime] = mglFillRect(x, y, size, color, antialiasing) +% usage: results = mglFillRect(x, y, size, color, antialiasing) % by: Benjamin Heasly % date: 03-17--2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -27,7 +27,7 @@ %mglFillRect(x, y, sz, [1 1 0]); %mglFlush(); % -function [ackTime, processedTime] = mglFillRect(x, y, size, color, antialiasing) +function results = mglFillRect(x, y, size, color, antialiasing) nDots = numel(x); if ~isequal(numel(y), nDots) @@ -68,6 +68,6 @@ border(:) = antialiasing; shape = zeros(1, nDots); -[ackTime, processedTime] = mglMetalDots(xyz, rgba, wh, shape, border); +results = mglMetalDots(xyz, rgba, wh, shape, border); diff --git a/mgllib/mglFillRect3D.m b/mgllib/mglFillRect3D.m index 13ccc802..f4a9f763 100644 --- a/mgllib/mglFillRect3D.m +++ b/mgllib/mglFillRect3D.m @@ -1,6 +1,6 @@ % mglFillRect3D - draw filled rectangle(s) on the screen % -% usage: [ackTime, processedTime] = mglFillRect3D(x, y, z, size, [rgb], [rotation], [antialias]) +% usage: results = mglFillRect3D(x, y, z, size, [rgb], [rotation], [antialias]) % by: Benjamin Heasly % date: 03-17-2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -28,7 +28,7 @@ % rotData = [45 0 1 0]; % 45 degrees about the y-axis. % mglFillRect3D(x, y, z, sz, [1 1 0], rotData); % mglFlush(); -function [ackTime, processedTime] = mglFillRect3D(x, y, z, size, rgb, rotation, antialias, socketInfo) +function results = mglFillRect3D(x, y, z, size, rgb, rotation, antialias, socketInfo) if nargin < 4 help mglFillRect3D @@ -118,7 +118,7 @@ ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(nVertices)); mglSocketWrite(socketInfo, vertexData); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); % https://www.cs.sfu.ca/~haoz/teaching/htmlman/rotate.html diff --git a/mgllib/mglFlush.m b/mgllib/mglFlush.m index 36b08e62..938267f6 100644 --- a/mgllib/mglFlush.m +++ b/mgllib/mglFlush.m @@ -1,17 +1,17 @@ % mglFLush: Commit a frame of drawing commands and wait for the next frame. % -% usage: [ackTime, processedTime] = mglFlush(socketInfo) +% usage: results = mglFlush(socketInfo) % by: justin gardner % date: 09/27/2021 % copyright: (c) 2021 Justin Gardner (GPL see mgl/COPYING) % purpose: Commit recent drawing commands and wait for the next frame. -% usage: waitTime = mglFlush(); +% usage: results = mglFlush(); % e.g.: mglOpen; % mglClearScreen([1 0 0]); % mglFlush; % mglClearScreen([0 1 0]); % mglFlush; -function [ackTime, processedTime] = mglFlush(socketInfo) +function results = mglFlush(socketInfo) if nargin < 1 || isempty(socketInfo) global mgl @@ -21,13 +21,10 @@ % write flush comnand and wait for return value. mglSocketWrite(socketInfo, socketInfo(1).command.mglFlush); ackTime = mglSocketRead(socketInfo, 'double'); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); % check if processedTime is negative which indicates an error -if processedTime < 0 +if any([results.processedTime] < 0) % display error - mglPrivateDisplayProcessingError(socketInfo, ackTime, processedTime, mfilename); + mglPrivateDisplayProcessingError(socketInfo, results, mfilename); end - - - diff --git a/mgllib/mglFrameGrab.m b/mgllib/mglFrameGrab.m index 2c997de9..bed2f72c 100644 --- a/mgllib/mglFrameGrab.m +++ b/mgllib/mglFrameGrab.m @@ -37,12 +37,11 @@ % % % display it % image(frame(:,:,1:3)) -function [frame, ackTime, processedTime] = mglFrameGrab(grabMode) +function [frame, results] = mglFrameGrab(grabMode) % default values for return variables frame = []; -ackTime = []; -processedTime = []; +results = []; % check whether mgl is running if ~mglMetalIsRunning @@ -124,7 +123,7 @@ frame = permute(frame,[2 3 1]); %mglSocketWrite(socketInfo, single(v)); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); %%%%%%%%%%%%%%%%%%%%%%%% diff --git a/mgllib/mglGetErrorMessage.m b/mgllib/mglGetErrorMessage.m index 5e3d59c3..b0c7b955 100644 --- a/mgllib/mglGetErrorMessage.m +++ b/mgllib/mglGetErrorMessage.m @@ -5,7 +5,7 @@ % date: 01/26/23 % purpose: Gets an error message from the mglMetal app % -function [msg, ackTime, processedTime] = mglGetErrorMessage(socketInfo) +function [msg, results] = mglGetErrorMessage(socketInfo) if nargin < 1 || isempty(socketInfo) global mgl @@ -23,7 +23,7 @@ msg = mglSocketReadString(socketInfo); % get the processed Time -processedTime = mglSocketRead(socketInfo, 'double'); -if processedTime < 0 +results = mglReadCommandResults(socketInfo, ackTime); +if any([results.processedTime] < 0) disp(sprintf('(mglGetErrorMessage) Error processing command.')); end diff --git a/mgllib/mglGluAnnulus.m b/mgllib/mglGluAnnulus.m index 420cf789..cf15eb47 100644 --- a/mgllib/mglGluAnnulus.m +++ b/mgllib/mglGluAnnulus.m @@ -1,7 +1,7 @@ % mglGluAnnulus - draw annulus/annuli at location x,y % % $Id$ -% usage: [ackTime, processedTime] = mglGluAnnulus(x, y, isize, osize, color, [nslices], [nloops], [antialiasing]) +% usage: results = mglGluAnnulus(x, y, isize, osize, color, [nslices], [nloops], [antialiasing]) % by: Benjamin Heasly % date: 03-17-2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -25,7 +25,7 @@ %colors = jet(4)'; %mglGluAnnulus(x, y, isize, osize, colors); %mglFlush(); -function [ackTime, processedTime] = mglGluAnnulus(x, y, isize, osize, color, nSlices, nLoops, antialiasing) +function results = mglGluAnnulus(x, y, isize, osize, color, nSlices, nLoops, antialiasing) % These are no longer needed in Metal, but included for v2 code compatibility. nSlices = []; @@ -80,4 +80,4 @@ wedge = zeros(2, nDots, 'single'); wedge(2,:) = 2*pi; -[ackTime, processedTime] = mglMetalArcs(xyz, rgba, radii, wedge, border); +results = mglMetalArcs(xyz, rgba, radii, wedge, border); diff --git a/mgllib/mglGluDisk.m b/mgllib/mglGluDisk.m index 27cc997e..a177e61d 100644 --- a/mgllib/mglGluDisk.m +++ b/mgllib/mglGluDisk.m @@ -1,7 +1,7 @@ % mglGluDisk - draw disk(s) at location x,y; alternative to glPoints for circular dots % % $Id$ -% usage: [ackTime, processedTime] = mglGluDisk(x, y, size, color, [nslices], [nloops], [antialiasing]) +% usage: results = mglGluDisk(x, y, size, color, [nslices], [nloops], [antialiasing]) % by: Benjamin Heasly % date: 03-17-2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -22,7 +22,7 @@ %y = 12*rand(100,1)-6; %mglGluDisk(x, y, 0.1, [0.1 0.6 1], 24, 2); %mglFlush(); -function [ackTime, processedTime] = mglGluDisk(x, y, size, color, nSlices, nLoops, antialiasing) +function results = mglGluDisk(x, y, size, color, nSlices, nLoops, antialiasing) % These are no longer needed in Metal, but included for v2 code compatibility. nSlices = []; @@ -75,4 +75,4 @@ border(:) = antialiasing; % run arcs command -[ackTime, processedTime] = mglMetalArcs(xyz,rgba,radii,wedge,border); +results = mglMetalArcs(xyz,rgba,radii,wedge,border); diff --git a/mgllib/mglGluPartialDisk.m b/mgllib/mglGluPartialDisk.m index ac0d9a34..fcbc1f2f 100644 --- a/mgllib/mglGluPartialDisk.m +++ b/mgllib/mglGluPartialDisk.m @@ -1,7 +1,7 @@ % mglGluPartialDisk - draw partial disk(s) at location x,y % % $Id$ -% usage: [ackTime, processedTime] = mglGluPartialDisk(x, y, isize, osize, startAngles, sweepAngles, color, [nslices], [nloops], antialiasing) +% usage: results = mglGluPartialDisk(x, y, isize, osize, startAngles, sweepAngles, color, [nslices], [nloops], antialiasing) % by: Benjamin Heasly % date: 2022-03-17 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -31,7 +31,7 @@ %colors = jet(10)'; %mglGluPartialDisk(x, y, isize, osize, startAngles, sweepAngles, colors, 60, 2); %mglFlush(); -function [ackTime, processedTime] = mglGluPartialDisk(x, y, isize, osize, startAngles, sweepAngles, color, nSlices, nLoops, antialiasing) +function results = mglGluPartialDisk(x, y, isize, osize, startAngles, sweepAngles, color, nSlices, nLoops, antialiasing) % These are no longer needed in Metal, but included for v2 code compatibility. nSlices = []; @@ -94,4 +94,4 @@ border = zeros(1, nDots, 'single'); border(:) = antialiasing; -[ackTime, processedTime] = mglMetalArcs(xyz, rgba, radii, wedge, border); +results = mglMetalArcs(xyz, rgba, radii, wedge, border); diff --git a/mgllib/mglInfo.m b/mgllib/mglInfo.m index 42823a94..5f3a2560 100644 --- a/mgllib/mglInfo.m +++ b/mgllib/mglInfo.m @@ -1,6 +1,6 @@ % mglFLush: Commit a frame of drawing commands and wait for the next frame. % -% usage: [info, ackTime, processedTime] = mglInfo(socketInfo) +% usage: [info, results] = mglInfo(socketInfo) % by: justin gardner % date: 01/26/2023 % copyright: (c) 2021 Justin Gardner (GPL see mgl/COPYING) @@ -9,7 +9,7 @@ % e.g.: mglOpen; % info = mglInfo; % -function [info,ackTime, processedTime] = mglInfo(socketInfo) +function [info, results] = mglInfo(socketInfo) % set return structure info = []; @@ -18,7 +18,7 @@ socketInfo = mglGetParam('activeSockets'); % no open mgl window, return if isempty(socketInfo) - ackTime = -mglGetSecs;processedTime = -mglGetSecs; + results = []; disp(sprintf('(%s) No open mgl window',mfilename)); return end @@ -77,8 +77,7 @@ dataType = mglSocketRead(socketInfo, 'uint16'); end -processedTime = mglSocketRead(socketInfo, 'double'); -if processedTime < 0 +results = mglReadCommandResults(socketInfo, ackTime); +if any([results.processedTime] < 0) disp(sprintf('(mglFlush) Error processing command.')); end - diff --git a/mgllib/mglLines2.m b/mgllib/mglLines2.m index 51363587..8caf1630 100644 --- a/mgllib/mglLines2.m +++ b/mgllib/mglLines2.m @@ -21,7 +21,7 @@ %mglVisualAngleCoordinates(57,[16 12]); %mglLines2(rand(1,100)*5-2.5, rand(1,100)*10-5, rand(1,100)*5-2.5, rand(1,100)*3-1.5, 3, [0 0.6 1],1); %mglFlush -function [ackTime, processedTime] = mglLines2(x0, y0, x1, y1, size, color, antialiasing, socketInfo) +function results = mglLines2(x0, y0, x1, y1, size, color, antialiasing, socketInfo) % antialiasing is ignored, but included for v2 parameter compatibility. antialiasing = []; @@ -53,4 +53,4 @@ ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(2*iLine)); mglSocketWrite(socketInfo, single(v)); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglMetalArcs.m b/mgllib/mglMetalArcs.m index 11a1cb1d..35ae0988 100644 --- a/mgllib/mglMetalArcs.m +++ b/mgllib/mglMetalArcs.m @@ -1,12 +1,12 @@ % mglMetalArcs.m % % $Id$ -% usage: [ackTime, processedTime] = mglMetalArcs(xyz, rgba, radii, wedge, border) +% usage: results = mglMetalArcs(xyz, rgba, radii, wedge, border) % by: Benjamin heasly % date: 03/17/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) % purpose: Draw "vectorized" arcs with xyz, rgba, radii, wedge -% usage: [ackTime, processedTime] = mglMetalArcs(xyz, rgba, wh, radii, wedge, border) +% usage: results = mglMetalArcs(xyz, rgba, wh, radii, wedge, border) % This is a common interface to mglMeal for circular arcs. % xyz -- 3 x n matrix of point positions [x, y, z] % rgba -- 4 x n matrix of point colors [r, g, b, a] @@ -26,7 +26,7 @@ % border = [0.1 0.1 0.1]; % mglMetalArcs(xyz, rgba, radii, wedge, border); % mglFlush(); -function [ackTime, processedTime] = mglMetalArcs(xyz, rgba, radii, wedge, border, socketInfo) +function results = mglMetalArcs(xyz, rgba, radii, wedge, border, socketInfo) if nargin < 5 help mglMetalArcs @@ -62,4 +62,4 @@ ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(nDots)); mglSocketWrite(socketInfo, vertexData); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglMetalBltTexture.m b/mgllib/mglMetalBltTexture.m index 89d555b3..283c68c7 100644 --- a/mgllib/mglMetalBltTexture.m +++ b/mgllib/mglMetalBltTexture.m @@ -1,6 +1,6 @@ % mglMetalBltTexture.m % -% usage: [ackTime, processedTime, setupTime] = mglMetalBltTexture(tex, position, hAlignment, vAlignment, rotation, phase, width, height, socketInfo) +% usage: results = mglMetalBltTexture(tex, position, hAlignment, vAlignment, rotation, phase, width, height, socketInfo) % by: justin gardner % date: 09/28/2021 % copyright: (c) 2021 Justin Gardner (GPL see mgl/COPYING) @@ -17,13 +17,11 @@ % mglMetalBltTexture(imageTex,[0 0]); % mglFlush(); % -function [ackTime, processedTime, setupTime] = mglMetalBltTexture(tex, position, hAlignment, vAlignment, rotation, phase, width, height, socketInfo) +function results = mglMetalBltTexture(tex, position, hAlignment, vAlignment, rotation, phase, width, height, socketInfo) % empty image, nothing to do. if isempty(tex) - ackTime = mglGetSecs; - processedTime = mglGetSecs; - setupTime = mglGetSecs; + results = []; return end @@ -59,7 +57,7 @@ socketInfo = mglGetParam('activeSockets'); % no open mgl window, return if isempty(socketInfo) - ackTime = -mglGetSecs;processedTime = -mglGetSecs;setupTime = -mglGetSecs; + results = []; disp(sprintf('(%s) No open mgl window',mfilename)); return end @@ -146,14 +144,10 @@ mglSocketWrite(socketInfo, single(verticesWithTextureCoordinates)); mglSocketWrite(socketInfo, single(phase)); mglSocketWrite(socketInfo, tex.textureNumber); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime, setupTime); % check if processedTime is negative which indicates an error -if processedTime < 0 +if any([results.processedTime] < 0) % display error - mglPrivateDisplayProcessingError(socketInfo, ackTime, processedTime, mfilename); + mglPrivateDisplayProcessingError(socketInfo, results, mfilename); end - - - - diff --git a/mgllib/mglMetalCreateTexture.m b/mgllib/mglMetalCreateTexture.m index e173b7c2..81dde73f 100644 --- a/mgllib/mglMetalCreateTexture.m +++ b/mgllib/mglMetalCreateTexture.m @@ -1,6 +1,6 @@ % mglMetalCreateTexture.m % -% usage: [tex, ackTime, processedTime] = mglMetalCreateTexture(im, [minMagFilter, mipFilter, addressMode]) +% usage: [tex, results] = mglMetalCreateTexture(im, [minMagFilter, mipFilter, addressMode]) % by: justin gardner % date: 09/28/2021 % copyright: (c) 2021 Justin Gardner (GPL see mgl/COPYING) @@ -32,13 +32,12 @@ % and/or mglMirrorActivate, returns a struct arrauy with one % element per active mirror. % -function [tex, ackTime, processedTime] = mglMetalCreateTexture(im, minMagFilter, mipFilter, addressMode, socketInfo) +function [tex, results] = mglMetalCreateTexture(im, minMagFilter, mipFilter, addressMode, socketInfo) % empty image, nothing to do. if isempty(im) tex = []; - ackTime = mglGetSecs; - processedTime = mglGetSecs; + results = []; return end @@ -98,19 +97,19 @@ % Check each socket for processing results. responseIncoming = mglSocketRead(socketInfo, 'double'); tex = repmat(tex, 1, numel(socketInfo)); -processedTime = zeros([1, numel(socketInfo)]); +resultCell = cell([1, numel(socketInfo)]); numTextures = zeros([1, numel(socketInfo)]); for ii = 1:numel(socketInfo) if (responseIncoming(ii) < 0) % This socket shows an error processing the command. tex(ii).textureNumber = -1; - processedTime(ii) = mglSocketRead(socketInfo(ii), 'double'); + resultCell{ii} = mglReadCommandResults(socketInfo(ii), ackTime(1,1,1,ii)); fprintf('(mglMetalCreateTexture) Error creating Metal texture, you might try again with Console running, or: log stream --level info --process mglMetal\n'); else % This socket shows processing was OK, read the response. tex(ii).textureNumber = mglSocketRead(socketInfo(ii), 'uint32'); numTextures(ii) = mglSocketRead(socketInfo(ii), 'uint32'); - processedTime(ii) = mglSocketRead(socketInfo(ii), 'double'); + resultCell{ii} = mglReadCommandResults(socketInfo(ii), ackTime(1,1,1,ii)); end % Only update the mgl context from the primary window. @@ -118,3 +117,4 @@ mglSetParam('numTextures', numTextures(ii)); end end +results = [resultCell{:}]; diff --git a/mgllib/mglMetalDots.m b/mgllib/mglMetalDots.m index 0335fcaa..34cb6954 100644 --- a/mgllib/mglMetalDots.m +++ b/mgllib/mglMetalDots.m @@ -1,12 +1,12 @@ % mglMetalDots.m % % $Id$ -% usage: [ackTime, processedTime, setupTime] = mglMetalDots(xyz, rgba, wh, shape, border) +% usage: results = mglMetalDots(xyz, rgba, wh, shape, border) % by: Benjamin heasly % date: 03/16/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) % purpose: Draw "vectorized" dots with xyz, rgba, width and height. -% usage: [ackTime, processedTime] = mglMetalDots(xyz, rgba, wh, shape, border) +% usage: results = mglMetalDots(xyz, rgba, wh, shape, border) % This is a common interface to mglMeal for points, dots, etc. % xyz -- 3 x n matrix of point positions [x, y, z] % rgba -- 4 x n matrix of point colors [r, g, b, a] @@ -14,7 +14,7 @@ % shape -- 1 x n shape 0 -> rectangle, 1-> oval % border-- 1 x n antialiasing pixel size % -function [ackTime, processedTime, setupTime] = mglMetalDots(xyz, rgba, wh, shape, border, socketInfo) +function results = mglMetalDots(xyz, rgba, wh, shape, border, socketInfo) if nargin < 5 help mglMetalDots @@ -51,4 +51,4 @@ ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(nDots)); mglSocketWrite(socketInfo, vertexData); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime, setupTime); diff --git a/mgllib/mglMetalFinishBatch.m b/mgllib/mglMetalFinishBatch.m new file mode 100644 index 00000000..c7f79b91 --- /dev/null +++ b/mgllib/mglMetalFinishBatch.m @@ -0,0 +1,78 @@ +% mglMetalFinishBatch: await results from a completed command batch. +% +% usage: [results, batchInfo]= mglMetalFinishBatch(batchInfo) +% by: ben heasly +% date: 01/12/2024 +% copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) +% purpose: Returns the Mgl Metal to its normal state +% usage: [results, batchInfo]= mglMetalFinishBatch(batchInfo) +% +% Inputs: +% - batchInfo - a struct of batch info and timestamps, as +% from mglMetalStartBatch() +% +% Returns: +% - results - an array of timing results with one element per +% command in the processed batch +% - batchInfo - the given struct with a processing finish +% timestamp +% +% After processing a command batch, following +% mglMetalStartBatch() and mglMetalProcessBatch(), the Mgl +% Metal app will send back a number indicating the number of +% command reponses that are waiting. Use this function to +% wait for that number and retrieve the pending responses. +% +% % Here's a command batch example. +% mglOpen(); +% batchInfo = mglMetalStartBatch(); +% +% % Queue up three frames, each with a different clear color. +% % This part is all communication and no processing. +% mglClearScreen([1 0 0]); +% mglFlush(); +% mglClearScreen([0 1 0]); +% mglFlush(); +% mglClearScreen([0 0 1]); +% mglFlush(); +% +% % Let the Mgl Metal app process the batch as fast as if can. +% % This part is all processing and no communication. +% batchInfo = mglMetalProcessBatch(batchInfo); +% +% % Potentially do other things while the batch is going. +% disp('A batch is running asynchronously!') +% +% % Wait for the batch to finish. +% % This should give us 6 results, one for each queued command. +% % Passing in batchInfo converts GPU timestamps to CPU time. +% results = mglMetalFinishBatch(batchInfo) +% +% mglClose(); +function [results, batchInfo] = mglMetalFinishBatch(batchInfo, socketInfo) + +global mgl +if nargin < 2 || isempty(socketInfo) + socketInfo = mgl.activeSockets; +end + +% Await the signal that asynchronous batch processing is complete. +commandCount = mglSocketRead(socketInfo, 'uint32'); + +% Request all the command results. +mglSocketWrite(socketInfo, socketInfo(1).command.mglFinishBatch); +batchInfo.finishAckTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, [], [], commandCount); + +% Convert GPU timestamp results to CPU time. +if isfield(batchInfo, 'cpuBefore') && isfield(batchInfo, 'gpuBefore') + [cpuAfter, gpuAfter] = mglMetalSampleTimestamps(socketInfo); + batchInfo.cpuAfter = cpuAfter; + batchInfo.gpuAfter = gpuAfter; + results = mglConvertCommandResults( ... + results, ... + batchInfo.cpuBefore, ... + batchInfo.gpuBefore, ... + batchInfo.cpuAfter, ... + batchInfo.gpuAfter); +end diff --git a/mgllib/mglMetalFullscreen.m b/mgllib/mglMetalFullscreen.m index 548e6514..7ed962c4 100644 --- a/mgllib/mglMetalFullscreen.m +++ b/mgllib/mglMetalFullscreen.m @@ -1,6 +1,6 @@ % mglMetalFullscreen.m % -% usage: [ackTime, processedTime] = mglMetalFullscreen(isFullscreen, socketInfo) +% usage: results = mglMetalFullscreen(isFullscreen, socketInfo) % by: Benjamin Heasly % date: 27 April 2022 % copyright: (c) 2021 Justin Gardner (GPL see mgl/COPYING) @@ -24,7 +24,7 @@ % mglMetalFullscreen(true, socketInfo); % pause(5); % mglMetalFullscreen(false, socketInfo); -function [ackTime, processedTime] = mglMetalFullscreen(isFullscreen, socketInfo) +function results = mglMetalFullscreen(isFullscreen, socketInfo) if nargin < 1 isFullscreen = true; @@ -46,7 +46,7 @@ mglSocketWrite(socketInfo, socketInfo.command.mglWindowed); end ackTime = mglSocketRead(socketInfo, 'double'); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); % Only update the mgl context from the primary window. if isfield(mgl, 's') && isequal(socketInfo, mgl.s) diff --git a/mgllib/mglMetalGetWindowFrameInDisplay.m b/mgllib/mglMetalGetWindowFrameInDisplay.m index c3f52fa6..4c95a1d3 100644 --- a/mgllib/mglMetalGetWindowFrameInDisplay.m +++ b/mgllib/mglMetalGetWindowFrameInDisplay.m @@ -1,6 +1,6 @@ % mglMetalGetWindowFrameInDisplay.m % -% usage: [displayNumber, rect, ackTime, processedTime] = mglMetalGetWindowFrameInDisplay(socketInfo) +% usage: [displayNumber, rect, results] = mglMetalGetWindowFrameInDisplay(socketInfo) % by: Benjamin Heasly % date: 03/11/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -15,7 +15,7 @@ % % socketInfo = mglMirrorOpen(0); % [displayNumber, rect] = mglMetalGetWindowFrameInDisplay(socketInfo) % -function [displayNumber, rect, ackTime, processedTime] = mglMetalGetWindowFrameInDisplay(socketInfo) +function [displayNumber, rect, results] = mglMetalGetWindowFrameInDisplay(socketInfo) global mgl if nargin < 1 || isempty(socketInfo) @@ -28,13 +28,17 @@ % Check if the command was processed OK or with error, for each socket. responseIncoming = mglSocketRead(socketInfo, 'double'); -processedTime = zeros([1, numel(socketInfo)]); +resultCell = cell([1, numel(socketInfo)]); displayNumber = zeros([1, numel(socketInfo)]); +x = zeros([1, numel(socketInfo)]); +y = zeros([1, numel(socketInfo)]); +width = zeros([1, numel(socketInfo)]); +height = zeros([1, numel(socketInfo)]); rect = zeros(numel(socketInfo), 4); for ii = 1:numel(socketInfo) if (responseIncoming(ii) < 0) % This socket shows an error processing the command. - processedTime(ii) = mglSocketRead(socketInfo(ii), 'double'); + resultCell{ii} = mglReadCommandResults(socketInfo(ii), ackTime(1, 1, 1, ii)); fprintf('(mglMetalGetWindowFrameInDisplay) Error getting Metal window and display info, you might try again with Console running, or: log stream --level info --process mglMetal\n'); else % This socket shows processing was OK, read the response. @@ -43,7 +47,7 @@ y = mglSocketRead(socketInfo(ii), 'uint32'); width = mglSocketRead(socketInfo(ii), 'uint32'); height = mglSocketRead(socketInfo(ii), 'uint32'); - processedTime = mglSocketRead(socketInfo(ii), 'double'); + resultCell{ii} = mglReadCommandResults(socketInfo(ii), ackTime(1, 1, 1, ii)); rect(ii,:) = [x, y, width, height]; end @@ -56,3 +60,4 @@ mglSetParam('screenHeight', height); end end +results = [resultCell{:}]; diff --git a/mgllib/mglMetalLines.m b/mgllib/mglMetalLines.m index 57de699d..0db932b8 100644 --- a/mgllib/mglMetalLines.m +++ b/mgllib/mglMetalLines.m @@ -24,7 +24,7 @@ % mglVisualAngleCoordinates(57,[16 12]); % mglMetalLines(rand(1,10)*5-2.5, rand(1,10)*10-5, rand(1,10)*5-2.5, rand(1,10)*3-1.5, 0.5, [0 0.6 1]'); % mglFlush; -function [ackTime, processedTime] = mglMetalLines(x0, y0, x1, y1, lineWidth, color) +function results = mglMetalLines(x0, y0, x1, y1, lineWidth, color) nLines = numel(x0); if ~isequal(numel(y0), nLines) || ~isequal(numel(x1), nLines) || ~isequal(numel(y1), nLines) @@ -57,4 +57,4 @@ % Pack up and draw the widened lines as quads. quadX = cat(1, x0 + offsetX, x1 + offsetX, x1 - offsetX, x0 - offsetX); quadY = cat(1, y0 + offsetY, y1 + offsetY, y1 - offsetY, y0 - offsetY); -[ackTime, processedTime] = mglQuad(quadX, quadY, color); +results = mglQuad(quadX, quadY, color); diff --git a/mgllib/mglMetalProcessBatch.m b/mgllib/mglMetalProcessBatch.m new file mode 100644 index 00000000..bce3fa78 --- /dev/null +++ b/mgllib/mglMetalProcessBatch.m @@ -0,0 +1,68 @@ +% mglMetalProcessBatch: start processing Metal commands enqueued earlier. +% +% usage: batchInfo = mglMetalProcessBatch(batchInfo) +% by: ben heasly +% date: 01/12/2024 +% copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) +% purpose: Put the Mgl Metal app into its batch "processing" state. +% usage: batchInfo = mglMetalProcessBatch(batchInfo) +% +% Inputs: +% - batchInfo - a struct of batch info and timestamps, as +% from mglMetalStartBatch() +% +% Returns: +% - batchInfo - the given struct with a processing start +% timestamp +% +% In batch "processing" state, the MglMetal is focused on +% command processing and rendering but not communicating with +% Matlab. Commands that were queued up previously, following +% mglMetalStartBatch(), will be processed as fast as the app is +% able to. Command responses will be queued up in the same +% order, to be returned later via mglMetalFinishBatch(). +% +% During batch processing, the Mgl Metal app will operate +% asynchronously -- it's possible to do other things in Matlab +% at the same time. +% +% When batch processing is complete the Mgl Metal app will +% send back a number indicating how many command reponses are +% waiting. Use mglMetalFinishBatch() to wait for this number +% and retrieve the pending responses. +% +% % Here's a command batch example. +% mglOpen(); +% batchInfo = mglMetalStartBatch(); +% +% % Queue up three frames, each with a different clear color. +% % This part is all communication and no processing. +% mglClearScreen([1 0 0]); +% mglFlush(); +% mglClearScreen([0 1 0]); +% mglFlush(); +% mglClearScreen([0 0 1]); +% mglFlush(); +% +% % Let the Mgl Metal app process the batch as fast as if can. +% % This part is all processing and no communication. +% batchInfo = mglMetalProcessBatch(batchInfo); +% +% % Potentially do other things while the batch is going. +% disp('A batch is running asynchronously!') +% +% % Wait for the batch to finish. +% % This should give us 6 results, one for each queued command. +% % Passing in batchInfo converts GPU timestamps to CPU time. +% results = mglMetalFinishBatch(batchInfo) +% +% mglClose(); +function batchInfo = mglMetalProcessBatch(batchInfo, socketInfo) + +global mgl +if nargin < 2 || isempty(socketInfo) + socketInfo = mgl.activeSockets; +end + +mglSocketWrite(socketInfo, socketInfo(1).command.mglProcessBatch); +batchInfo.processAckTime = mglSocketRead(socketInfo, 'double'); diff --git a/mgllib/mglMetalReadTexture.m b/mgllib/mglMetalReadTexture.m index bc836694..a8b0e19e 100644 --- a/mgllib/mglMetalReadTexture.m +++ b/mgllib/mglMetalReadTexture.m @@ -1,6 +1,6 @@ % mglMetalReadTexture.m % -% usage: [im, ackTime, processedTime] = mglMetalReadTexture(tex) +% usage: [im, results] = mglMetalReadTexture(tex) % by: Ben Heasly % date: 03/04/2022 % copyright: (c) 2021 Justin Gardner (GPL see mgl/COPYING) @@ -28,7 +28,7 @@ % an extra dimension indicating which mirror the image came from % [height, width, 4, mirorIndex]. % -function [im, ackTime, processedTime] = mglMetalReadTexture(tex, socketInfo) +function [im, results] = mglMetalReadTexture(tex, socketInfo) if numel(tex) > 1 fprintf('(mglMetalReadTexture) Only using the first of %d elements of tex struct array. To avoid this warning pass in tex(1) instead.\n', numel(tex)); @@ -47,12 +47,12 @@ % Check each socket for processing results. responseIncoming = mglSocketRead(socketInfo, 'double'); -processedTime = zeros([1, numel(socketInfo)]); +resultCell = cell([1, numel(socketInfo)]); textureData = zeros([4, tex.imageWidth, tex.imageHeight, numel(socketInfo)]); for ii = 1:numel(socketInfo) if (responseIncoming(ii) < 0) % This socket shows an error processing the command. - processedTime(ii) = mglSocketRead(socketInfo(ii), 'double'); + resultCell{ii} = mglReadCommandResults(socketInfo(ii), ackTime(1,1,1,ii)); fprintf("(mglMetalReadTexture) Error reading Metal texture, you might try again with Console running, or: log stream --level info --process mglMetal\n"); else % This socket shows processing was OK, read the image data. @@ -62,9 +62,10 @@ fprintf("(mglMetalReadTexture) Unexpected size for textureNumber %d -- expected width %d but got %d, expected height %d but got %d\n", tex.textureNumber, tex.imageWidth, width, tex.imageHeight, height); end textureData(:,:,:,ii) = mglSocketRead(socketInfo(ii), 'single', 4, width, height); - processedTime(ii) = mglSocketRead(socketInfo(ii), 'double'); + resultCell{ii} = mglReadCommandResults(socketInfo(ii), ackTime(1,1,1,ii)); end end +results = [resultCell{:}]; % Rearrange the textre data into the Matlab image format. diff --git a/mgllib/mglMetalRepeatingBlts.m b/mgllib/mglMetalRepeatingBlts.m index 85098521..a850036c 100644 --- a/mgllib/mglMetalRepeatingBlts.m +++ b/mgllib/mglMetalRepeatingBlts.m @@ -52,6 +52,11 @@ mglSocketWrite(socketInfo, socketInfo(1).command.mglRepeatBlts); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(nFrames)); -drawAndFrameTimes = mglSocketRead(socketInfo, 'double', 2 * nFrames); -drawTimes = drawAndFrameTimes(1:2:end); -frameTimes = drawAndFrameTimes(2:2:end); + +drawTimes = zeros(1, nFrames); +frameTimes = zeros(1, nFrames); +for ii = 1:nFrames + drawTimes(ii) = mglSocketRead(socketInfo, 'double'); + results = mglReadCommandResults(socketInfo); + frameTimes(ii) = results.processedTime; +end diff --git a/mgllib/mglMetalRepeatingDots.m b/mgllib/mglMetalRepeatingDots.m index 88cf7497..36073161 100644 --- a/mgllib/mglMetalRepeatingDots.m +++ b/mgllib/mglMetalRepeatingDots.m @@ -60,6 +60,11 @@ mglSocketWrite(socketInfo, uint32(nFrames)); mglSocketWrite(socketInfo, uint32(nDots)); mglSocketWrite(socketInfo, uint32(randomSeed)); -drawAndFrameTimes = mglSocketRead(socketInfo, 'double', 2 * nFrames); -drawTimes = drawAndFrameTimes(1:2:end); -frameTimes = drawAndFrameTimes(2:2:end); + +drawTimes = zeros(1, nFrames); +frameTimes = zeros(1, nFrames); +for ii = 1:nFrames + drawTimes(ii) = mglSocketRead(socketInfo, 'double'); + results = mglReadCommandResults(socketInfo); + frameTimes(ii) = results.processedTime; +end diff --git a/mgllib/mglMetalRepeatingFlicker.m b/mgllib/mglMetalRepeatingFlicker.m index ce8bd19b..280e854e 100644 --- a/mgllib/mglMetalRepeatingFlicker.m +++ b/mgllib/mglMetalRepeatingFlicker.m @@ -45,6 +45,11 @@ ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(nFrames)); mglSocketWrite(socketInfo, uint32(randomSeed)); -drawAndFrameTimes = mglSocketRead(socketInfo, 'double', 2 * nFrames); -drawTimes = drawAndFrameTimes(1:2:end); -frameTimes = drawAndFrameTimes(2:2:end); + +drawTimes = zeros(1, nFrames); +frameTimes = zeros(1, nFrames); +for ii = 1:nFrames + drawTimes(ii) = mglSocketRead(socketInfo, 'double'); + results = mglReadCommandResults(socketInfo); + frameTimes(ii) = results.processedTime; +end diff --git a/mgllib/mglMetalRepeatingFlush.m b/mgllib/mglMetalRepeatingFlush.m index 09400889..125772f6 100644 --- a/mgllib/mglMetalRepeatingFlush.m +++ b/mgllib/mglMetalRepeatingFlush.m @@ -41,6 +41,11 @@ mglSocketWrite(socketInfo, socketInfo(1).command.mglRepeatFlush); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(nFrames)); -drawAndFrameTimes = mglSocketRead(socketInfo, 'double', 2 * nFrames); -drawTimes = drawAndFrameTimes(1:2:end); -frameTimes = drawAndFrameTimes(2:2:end); + +drawTimes = zeros(1, nFrames); +frameTimes = zeros(1, nFrames); +for ii = 1:nFrames + drawTimes(ii) = mglSocketRead(socketInfo, 'double'); + results = mglReadCommandResults(socketInfo); + frameTimes(ii) = results.processedTime; +end diff --git a/mgllib/mglMetalRepeatingQuads.m b/mgllib/mglMetalRepeatingQuads.m index e95ef740..0261cd3e 100644 --- a/mgllib/mglMetalRepeatingQuads.m +++ b/mgllib/mglMetalRepeatingQuads.m @@ -58,6 +58,11 @@ mglSocketWrite(socketInfo, uint32(nFrames)); mglSocketWrite(socketInfo, uint32(nQuads)); mglSocketWrite(socketInfo, uint32(randomSeed)); -drawAndFrameTimes = mglSocketRead(socketInfo, 'double', 2 * nFrames); -drawTimes = drawAndFrameTimes(1:2:end); -frameTimes = drawAndFrameTimes(2:2:end); + +drawTimes = zeros(1, nFrames); +frameTimes = zeros(1, nFrames); +for ii = 1:nFrames + drawTimes(ii) = mglSocketRead(socketInfo, 'double'); + results = mglReadCommandResults(socketInfo); + frameTimes(ii) = results.processedTime; +end diff --git a/mgllib/mglMetalSampleTimestamps.m b/mgllib/mglMetalSampleTimestamps.m new file mode 100644 index 00000000..a25006e6 --- /dev/null +++ b/mgllib/mglMetalSampleTimestamps.m @@ -0,0 +1,28 @@ +% mglMetalSampleTimestamps.m +% +% usage: [cpu, gpu, results] = mglMetalSampleTimestamps(socketInfo) +% by: Benjamin Heasly +% date: 01/30/2024 +% copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) +% purpose: Get a CPU-GPU timestamp pair from the same moment. +% +% Returns: +% cpu: a CPU timestamp in seconds, comparable to mglGetSecs +% gpu: a GPU timestamp in unspecified units (GPU nanos?) +% +% % Get some samples! +% mglOpen(); +% [cpu, gpu] = mglMetalSampleTimestamps() +function [cpu, gpu, results] = mglMetalSampleTimestamps(socketInfo) + +global mgl +if nargin < 1 || isempty(socketInfo) + socketInfo = mgl.activeSockets; +end + +% Request a timestamp pair. +mglSocketWrite(socketInfo, socketInfo(1).command.mglSampleTimestamps); +ackTime = mglSocketRead(socketInfo, 'double'); +cpu = mglSocketRead(socketInfo, 'double'); +gpu = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglMetalSetRenderTarget.m b/mgllib/mglMetalSetRenderTarget.m index f5d5ef96..9a459412 100644 --- a/mgllib/mglMetalSetRenderTarget.m +++ b/mgllib/mglMetalSetRenderTarget.m @@ -1,6 +1,6 @@ % mglMetalSetRenderTarget.m % -% usage: [ackTime, processedTime] = mglMetalSetRenderTarget(tex) +% usage: results = mglMetalSetRenderTarget(tex) % by: Benjamin Heasly % date: 03/11/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -29,7 +29,7 @@ % mglMetalBltTexture(imageTex,[0 0]); % mglFlush(); % -function [ackTime, processedTime] = mglMetalSetRenderTarget(tex, socketInfo) +function results = mglMetalSetRenderTarget(tex, socketInfo) if (nargin < 1) renderTarget = uint32(0); @@ -49,4 +49,4 @@ mglSocketWrite(socketInfo, socketInfo(1).command.mglSetRenderTarget); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, renderTarget); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglMetalSetViewColorPixelFormat.m b/mgllib/mglMetalSetViewColorPixelFormat.m index 66ff047c..14c22f32 100644 --- a/mgllib/mglMetalSetViewColorPixelFormat.m +++ b/mgllib/mglMetalSetViewColorPixelFormat.m @@ -41,7 +41,9 @@ % % Restore the default format (no arg). % mglOpen(); % mglMetalSetViewColorPixelFormat(); -function [ackTime, processedTime] = mglMetalSetViewColorPixelFormat(formatIndex, socketInfo) +function results = mglMetalSetViewColorPixelFormat(formatIndex, socketInfo) + +results = []; if nargin < 1 formatIndex = 0; @@ -51,7 +53,6 @@ socketInfo = mglGetParam('activeSockets'); % no open mgl window, return if isempty(socketInfo) - ackTime = -mglGetSecs;processedTime = -mglGetSecs; disp(sprintf('(%s) No open mgl window',mfilename)); return end @@ -65,4 +66,5 @@ mglSocketWrite(socketInfo, socketInfo(1).command.mglSetViewColorPixelFormat); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(formatIndex)); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); + diff --git a/mgllib/mglMetalSetWindowFrameInDisplay.m b/mgllib/mglMetalSetWindowFrameInDisplay.m index 4406ee07..d16fcd9b 100644 --- a/mgllib/mglMetalSetWindowFrameInDisplay.m +++ b/mgllib/mglMetalSetWindowFrameInDisplay.m @@ -1,6 +1,6 @@ % mglMetalSetWindowFrameInDisplay.m % -% usage: [ackTime, processedTime] = mglMetalSetWindowFrameInDisplay(displayNumber, rect, socketInfo) +% usage: results = mglMetalSetWindowFrameInDisplay(displayNumber, rect, socketInfo) % by: Benjamin Heasly % date: 03/11/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -17,7 +17,7 @@ % % mglMetalSetWindowFrameInDisplay(1, [100, 200, 64, 48], socketInfo); % -function [ackTime, processedTime] = mglMetalSetWindowFrameInDisplay(displayNumber, rect, socketInfo) +function results = mglMetalSetWindowFrameInDisplay(displayNumber, rect, socketInfo) if nargin < 3 || isempty(socketInfo) global mgl @@ -41,4 +41,4 @@ mglSocketWrite(socketInfo, uint32(y)); mglSocketWrite(socketInfo, uint32(width)); mglSocketWrite(socketInfo, uint32(height)); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglMetalStartBatch.m b/mgllib/mglMetalStartBatch.m new file mode 100644 index 00000000..34252be6 --- /dev/null +++ b/mgllib/mglMetalStartBatch.m @@ -0,0 +1,64 @@ +% mglMetalStartBatch: start queueing up Metal commands to process later. +% +% usage: batchInfo = mglMetalStartBatch() +% by: ben heasly +% date: 01/12/2024 +% copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) +% purpose: Put the Mgl Metal app into its batch "building" state. +% usage: batchInfo = mglMetalStartBatch() +% +% Returns: +% - batchInfo - struct of info about the batch, including +% timestamps +% +% In batch "building" state, the MglMetal is focused on +% communicating with Matlab but not command processing or +% rendering. Commands sent to the app will be fully read, then +% saved in a queue for later processing. For each command, the +% client will get an immediate placeholder reponse, so that it +% doesn't block waiting for the real response. Real responses +% will be sent back all at once at the end of the batch, after +% mglMetalProcessBatch() and mglMetalFinishBatch(). +% +% % Here's a command batch example. +% mglOpen(); +% batchInfo = mglMetalStartBatch(); +% +% % Queue up three frames, each with a different clear color. +% % This part is all communication and no processing. +% mglClearScreen([1 0 0]); +% mglFlush(); +% mglClearScreen([0 1 0]); +% mglFlush(); +% mglClearScreen([0 0 1]); +% mglFlush(); +% +% % Let the Mgl Metal app process the batch as fast as if can. +% % This part is all processing and no communication. +% batchInfo = mglMetalProcessBatch(batchInfo); +% +% % Potentially do other things while the batch is going. +% disp('A batch is running asynchronously!') +% +% % Wait for the batch to finish. +% % This should give us 6 results, one for each queued command. +% % Passing in batchInfo converts GPU timestamps to CPU time. +% results = mglMetalFinishBatch(batchInfo) +% +% mglClose(); +function batchInfo = mglMetalStartBatch(socketInfo) + +global mgl +if nargin < 1 || isempty(socketInfo) + socketInfo = mgl.activeSockets; +end + +% Gather CPU and GPU timestamps just before the batch begins. +% We can use these later to convert GPU timestamps to CPU time. +[cpuBefore, gpuBefore] = mglMetalSampleTimestamps(socketInfo); +batchInfo.cpuBefore = cpuBefore; +batchInfo.gpuBefore = gpuBefore; + +% Put the Metal app into batch building mode and note the transition time. +mglSocketWrite(socketInfo, socketInfo(1).command.mglStartBatch); +batchInfo.startAckTime = mglSocketRead(socketInfo, 'double'); diff --git a/mgllib/mglMinimize.m b/mgllib/mglMinimize.m index 836be532..7a4bfff5 100644 --- a/mgllib/mglMinimize.m +++ b/mgllib/mglMinimize.m @@ -11,11 +11,10 @@ % mglOpen; % mglMinimize; % -function [ackTime, processedTime] = mglMinimize(restore) +function results = mglMinimize(restore) -% default values for return variables -ackTime = []; -processedTime = []; +% default values for return +results = []; % get socket global mgl; @@ -33,4 +32,4 @@ end % get processed time -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglMirrorFlushAll.m b/mgllib/mglMirrorFlushAll.m index de64f6b6..fae586e0 100644 --- a/mgllib/mglMirrorFlushAll.m +++ b/mgllib/mglMirrorFlushAll.m @@ -1,12 +1,12 @@ % mglMirrorFlushAll: Flush the primary and any mirrored windows. % % $Id$ -% usage: [ackTime, processedTime] = mglMirrorFlushAll() +% usage: results = mglMirrorFlushAll() % by: ben heasly % date: 09/07/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) % purpose: Flush the primary and any mirrored windows. -% usage: [ackTime, processedTime] = mglMirrorFlushAll() +% usage: results = mglMirrorFlushAll() % % This will flush the primary mgl window, plus any mirrored % windows, without causing a state change as to which windows @@ -21,13 +21,12 @@ % % % Clean up. % mglClose(); -function [ackTime, processedTime] = mglMirrorFlushAll() +function results = mglMirrorFlushAll() global mgl if isempty(mgl) - ackTime = []; - processedTime = []; + results= []; return; end socketInfo = cat(2, mgl.s, mgl.mirrorSockets); -[ackTime, processedTime] = mglFlush(socketInfo); +results = mglFlush(socketInfo); diff --git a/mgllib/mglPlotCommandResults.m b/mgllib/mglPlotCommandResults.m new file mode 100644 index 00000000..66ba3c85 --- /dev/null +++ b/mgllib/mglPlotCommandResults.m @@ -0,0 +1,101 @@ +% mglPlotCommandResults: plot a sequence of command/frame time records. +% +% usage: mglPlotCommandResults(flushResults) +% by: Benjamin Heasly +% date: 01/19/2024 +% copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) +% purpose: Visualize a sequence of detailed command and frame times. +% usage: mglPlotCommandResults(flushResults) +% +% Inputs: +% +% flushResults: struct array of command results from mglFlush commands, +% obtained over multiple frames. +% drawResults: struct array of command results from drawing commands, +% over the same frames as flushResults. +% +function f = mglPlotCommandResults(flushes, draws, figureName, refreshRate) + +if iscell(flushes) + flushes = [flushes{:}]; +end + +if nargin < 3 || isempty(figureName) + figureName = 'Frame Times'; +end + +if nargin < 4 || isempty(refreshRate) + displays = mglDescribeDisplays(); + refreshRate = displays(1).refreshRate; +end +expectedFrameMillis = 1000 / refreshRate; + +f = figure('Name', figureName); + +% Choose flush presentation time as the baseline. +% Since this is scheduled (in general) and reported (macOS 15.4+) by the +% system, I expect this to be the steadiest baseline for comparison. + +lastPresented = [flushes.drawablePresented]; + +% Show when each flush got presented -- which we learn from the next flush. +% Include the connecting line so we can see frames that went out of bounds. +line(1:(numel(lastPresented)-1), 1000 * diff(lastPresented), ... + 'LineStyle', '-', ... + 'Marker', 'o', ... + 'Color', 'magenta', ... + 'DisplayName', 'drawable presented'); + +% Show info about drawing commands, if any. +if nargin > 1 && ~isempty(draws) + if iscell(draws) + draws = [draws{:}]; + end + plotTimestamps(draws, lastPresented, 'setupTime', 'client setup', '.', 'cyan'); + plotTimestamps(draws, lastPresented, 'ackTime', 'draw ack', '.', 'black'); + plotTimestamps(draws, lastPresented, 'processedTime', 'draw done', 'o', 'black'); +end + +% Show lots of detail about flush commands! +plotTimestamps(flushes, lastPresented, 'vertexStart', 'vertex start', '.', 'red'); +plotTimestamps(flushes, lastPresented, 'vertexEnd', 'vertex end', '.', 'red'); +plotTimestamps(flushes, lastPresented, 'fragmentStart', 'fragment start', '.', 'green'); +plotTimestamps(flushes, lastPresented, 'fragmentEnd', 'fragment end', '.', 'green'); + +plotTimestamps(flushes, lastPresented, 'drawableAcquired', 'drawable acquired', '.', 'magenta'); + +plotTimestamps(flushes, lastPresented, 'ackTime', 'flush ack', '.', 'blue'); +plotTimestamps(flushes, lastPresented, 'processedTime', 'flush done', 'o', 'blue'); + +title('sub-frame timestamps wrt last drawable presentation') +grid('on'); +ylabel('ms'); +ylim(expectedFrameMillis * [-1.5 1.5]); +xlabel('frame number'); + +legend('AutoUpdate', 'off'); + +plotHorizontal(1, numel(flushes), expectedFrameMillis, 'red'); +plotHorizontal(1, numel(flushes), -expectedFrameMillis, 'red'); + + +%% Dig out one field of results and plot nonzero values wrt baseline. +function plotTimestamps(results, baseline, fieldName, displayName, marker, color) +xAxis = 1:numel(baseline); +timestamps = [results.(fieldName)]; +hasData = timestamps ~= 0; +line(xAxis(hasData), ... + 1000 * (timestamps(hasData) - baseline(hasData)), ... + 'LineStyle', 'none', ... + 'Marker', marker, ... + 'Color', color, ... + 'DisplayName', displayName); + + +%% Plot a horizontal line representing an expected frame time. +function plotHorizontal(left, right, height, color) +line([left, right], ... + height * [1 1], ... + 'Marker', 'none', ... + 'LineStyle', '--', ... + 'Color', color); diff --git a/mgllib/mglPoints2.m b/mgllib/mglPoints2.m index 4c1c2b21..53bbea56 100644 --- a/mgllib/mglPoints2.m +++ b/mgllib/mglPoints2.m @@ -1,12 +1,12 @@ % mglPoints2.m % % $Id$ -% usage: [ackTime, processedTime] = mglPoints2(x, y, size, color, isRound, antialiasing) +% usage: results = mglPoints2(x, y, size, color, isRound, antialiasing) % by: Benjamin Heasly % date: 04/17/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) % purpose: plot 2D points on an OpenGL screen opened with mglOpen -% usage: [ackTime, processedTime] = mglPoints2(x, y, size, color, isRound, antialiasing) +% usage: results = mglPoints2(x, y, size, color, isRound, antialiasing) % x,y = position of dots on screen % size = size of dots (device units, not pixels) % color of dots @@ -22,7 +22,7 @@ %mglPoints2(16*rand(500,1)-8,12*rand(500,1)-6,2,1); %mglFlush(); %mglClose(); -function [ackTime, processedTime] = mglPoints2(x, y, size, color, isRound, antialiasing) +function results = mglPoints2(x, y, size, color, isRound, antialiasing) nDots = numel(x); if ~isequal(numel(y), nDots) @@ -67,4 +67,4 @@ border = zeros(1, nDots, 'single'); border(:) = antialiasing; -[ackTime, processedTime] = mglMetalDots(xyz, rgba, wh, shape, border); \ No newline at end of file +results = mglMetalDots(xyz, rgba, wh, shape, border); \ No newline at end of file diff --git a/mgllib/mglPoints2c.m b/mgllib/mglPoints2c.m index 3928f22c..72f786f6 100644 --- a/mgllib/mglPoints2c.m +++ b/mgllib/mglPoints2c.m @@ -1,14 +1,14 @@ % mglPoints2c.m % % $Id$ -% usage: [ackTime, processedTime, setupTime] = mglPoints2c(x, y, size, r, g, b, a, isRound, antialiasing) +% usage: results = mglPoints2c(x, y, size, r, g, b, a, isRound, antialiasing) % by: Benjamin Heasly % date: 03/17/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson, Dan Birman (GPL see mgl/COPYING) % purpose: mex function to plot 2D points on the screen opened with mglOpen % allows every dot to have a different color, useful for overlapping % dot patches -% usage: [ackTime, processedTime, setupTime] = mglPoints2c(x, y, size, r, g, b, a, isRound, antialiasing) +% usage: results = mglPoints2c(x, y, size, r, g, b, a, isRound, antialiasing) % x,y = position of dots on screen % size = size of dots (device units, not pixels) % r,g,b = color of dots in 0->1 range @@ -23,7 +23,7 @@ % mglClearScreen % mglPoints2c(16*rand(500,1)-8,12*rand(500,1)-6,2,rand(500,1),rand(500,1),rand(500,1)); % mglFlush -function [ackTime, processedTime, setupTime] = mglPoints2c(x, y, size, r, g, b, a, isRound, antialiasing) +function results = mglPoints2c(x, y, size, r, g, b, a, isRound, antialiasing) nDots = numel(x); if ~isequal(numel(y), nDots) @@ -67,4 +67,4 @@ border = zeros(1, nDots, 'single'); border(:) = antialiasing; -[ackTime, processedTime, setupTime] = mglMetalDots(xyz, rgba, wh, shape, border); +results = mglMetalDots(xyz, rgba, wh, shape, border); diff --git a/mgllib/mglPoints3.m b/mgllib/mglPoints3.m index ac81fc0a..d82b5aea 100644 --- a/mgllib/mglPoints3.m +++ b/mgllib/mglPoints3.m @@ -1,12 +1,12 @@ % mglPoints3.m % % $Id$ -% usage: [ackTime, processedTime] = mglPoints3(x, y, z, size, color, isRound, antialiasing) +% usage: results = mglPoints3(x, y, z, size, color, isRound, antialiasing) % by: Benjamin Heasly % date: 04/17/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) % purpose: plot 3D points on a screen opened with mglOpen -% usage: [ackTime, processedTime] = mglPoints3(x, y, z, size, color, isRound, antialiasing) +% usage: results = mglPoints3(x, y, z, size, color, isRound, antialiasing) % x,y,z = position of dots on screen % size = size of dots (device units, not pixels) % color of dots @@ -20,7 +20,7 @@ %mglVisualAngleCoordinates(57,[16 12]); %mglPoints3(16*rand(500,1)-8,12*rand(500,1)-6,zeros(500,1),2,1); %mglFlush -function [ackTime, processedTime] = mglPoints3(x, y, z, size, color, isRound, antialiasing) +function results = mglPoints3(x, y, z, size, color, isRound, antialiasing) nDots = numel(x); if ~isequal(numel(y), nDots) @@ -71,4 +71,4 @@ border = zeros(1, nDots, 'single'); border(:) = antialiasing; -[ackTime, processedTime] = mglMetalDots(xyz, rgba, wh, shape, border); \ No newline at end of file +results = mglMetalDots(xyz, rgba, wh, shape, border); \ No newline at end of file diff --git a/mgllib/mglPolygon.m b/mgllib/mglPolygon.m index 31e6a02a..c51b5ba3 100644 --- a/mgllib/mglPolygon.m +++ b/mgllib/mglPolygon.m @@ -26,7 +26,7 @@ %mglPolygon(x, y, [1 0 0]); %mglFlush(); -function [ackTime, processedTime] = mglPolygon(x, y, color, socketInfo) +function results = mglPolygon(x, y, color, socketInfo) global mgl; @@ -74,4 +74,4 @@ ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(n)); mglSocketWrite(socketInfo, vertices); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglPrivateDisplayProcessingError.m b/mgllib/mglPrivateDisplayProcessingError.m index ace620d4..1e413513 100644 --- a/mgllib/mglPrivateDisplayProcessingError.m +++ b/mgllib/mglPrivateDisplayProcessingError.m @@ -1,7 +1,7 @@ % mglPrivateDisplayProcessingError.m % % $Id:$ -% usage: mglPrivateDisplayProcessingError(socketInfo, ackTime, processedTime, functionName) +% usage: mglPrivateDisplayProcessingError(socketInfo, results, functionName) % by: justin gardner % date: 01/27/23 % purpose: Private function that is used to handle errors that mglMetal returns @@ -9,7 +9,7 @@ % In this code, the error message is retrieved by querying mglMetal % and displayed. % -function retval = mglPrivateDisplayProcessingError(socketInfo, ackTime, processedTime, functionName) +function retval = mglPrivateDisplayProcessingError(socketInfo, results, functionName) % check arguments if ~any(nargin == [4]) @@ -22,5 +22,8 @@ disp(sprintf('mglMetal: %s',msg)); % display error message from the command that failed (i.e. the one calling this function) -disp(sprintf('(%s) mglMetal application reported error: took %0.3fs to process',functionName,-processedTime-ackTime)); - +for ii = 1:numel(results) + processedTime = results(ii).processedTime; + ackTime = results(ii).ackTime; + disp(sprintf('(%s) mglMetal application reported error: took %0.3fs to process',functionName,-processedTime-ackTime)); +end diff --git a/mgllib/mglQuad.m b/mgllib/mglQuad.m index f4653784..80609618 100644 --- a/mgllib/mglQuad.m +++ b/mgllib/mglQuad.m @@ -1,7 +1,7 @@ % mglQuad.m % % $Id$ -% usage: [ackTime, processedTime, setupTime] = mglQuad( vX, vY, rgbColor, [antiAliasFlag] ); +% usage: results = mglQuad( vX, vY, rgbColor, [antiAliasFlag] ); % by: justin gardner % date: 09/28/2021 % copyright: (c) 2021 Justin Gardner (GPL see mgl/COPYING) @@ -18,7 +18,7 @@ %mglScreenCoordinates %mglQuad([100; 600; 600; 100], [100; 200; 600; 100], [1; 1; 1], 1); %mglFlush(); -function [ackTime, processedTime, setupTime] = mglQuad(vX, vY, rgbColor, antiAliasFlag, socketInfo) +function results = mglQuad(vX, vY, rgbColor, antiAliasFlag, socketInfo) % Not currently used, but let's maintain compatibility with v2. if nargin < 4 @@ -63,4 +63,5 @@ ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(nVertices)); mglSocketWrite(socketInfo, single(v)); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime, setupTime); + diff --git a/mgllib/mglReadCommandResults.m b/mgllib/mglReadCommandResults.m new file mode 100644 index 00000000..9c757218 --- /dev/null +++ b/mgllib/mglReadCommandResults.m @@ -0,0 +1,103 @@ +% mglReadCommandResults: Read a results struct for Mgl Metal commands. +% +% usage: results = mglReadCommandResults(socketInfo, ackTime, setupTime, commandCount) +% by: Benjamin Heasly +% date: 01/19/2024 +% copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) +% purpose: Read a results struct for Mgl Metal commands. +% usage: results = mglReadCommandResults(socketInfo, ackTime, setupTime, commandCount) +% +% Inputs: +% +% socketInfo: array of socket info structs as from +% mglSocketCreateClient() or mgl.activeSockets +% ackTime: optional array of ack times previously collected +% from from Mgl Metal, to include with other results. +% setupTime: optional array of setup times previously collected, to +% include with other results. +% commandCount: optional number of processed commands expected from Mgl +% Metal -- defaults to 1 but may be more following a +% command batch. +% +% Output: +% +% results: struct array with fields describing command results +% including: +% - ackTime: an element of the given ackTime +% - setupTime: an element of the given setupTime +% - processedTime: completion timestamp reported by +% Mgl Metal, negative on error +% +% The results struct array will have one element per +% command and per socket, ie size will be +% [commandCount, numel(socketInfo)] +% +% This function only reads generic results that Mgl Metal returns for all +% commands, including command code, success or failure status, and several +% timestamps. +% +% This function does not read command-specific query results. Individual +% commands that expect query results should read those first, before +% calling this function to read in the generic results. +% +% One goal of this function is to present Mgl Metal command results in a +% consistent format across different situations including single command +% executions, batches of commands, and connections to multiple Mgl Metal +% instances. +% +% Another goal is to make it easier to make changes in how Mgl Metal +% reports command results. Instead of having to modify every Matlab +% function that talks to Mgl Metal, we should only have to modify this +% function to reflect the modified results in its struct array format. +function results = mglReadCommandResults(socketInfo, ackTime, setupTime, commandCount) + +if nargin < 4 || isempty(commandCount) + commandCount = 1; +end + +if nargin < 3 || isempty(setupTime) + setupTime = 0; +end + +if nargin < 2 || isempty(ackTime) + ackTime = 0; +end + +% Read results one field at a time, across all commands and sockets. +commandCode = mglSocketRead(socketInfo, 'uint16', commandCount); +success = mglSocketRead(socketInfo, 'uint32', commandCount); +processedTime = mglSocketRead(socketInfo, 'double', commandCount); +vertexStart = mglSocketRead(socketInfo, 'double', commandCount); +vertexEnd = mglSocketRead(socketInfo, 'double', commandCount); +fragmentStart = mglSocketRead(socketInfo, 'double', commandCount); +fragmentEnd = mglSocketRead(socketInfo, 'double', commandCount); +drawableAcquired = mglSocketRead(socketInfo, 'double', commandCount); +drawablePresented = mglSocketRead(socketInfo, 'double', commandCount); + +% Deal results to a struct array of size [commandCount, numel(socketInfo)]. +% mglSocketRead represents socket index as the 4th matrix dimension, to +% accommodate results with up to 3 data dimensions. Here we are +% expecting all scalar timestamps, so we can squeeze out any middle +% dimensions. +resultSize = [commandCount, numel(socketInfo)]; +results = struct( ... + 'commandCode', sizeForStruct(commandCode, resultSize), ... + 'success', sizeForStruct(success, resultSize), ... + 'ackTime', sizeForStruct(ackTime, resultSize), ... + 'setupTime', sizeForStruct(setupTime, resultSize), ... + 'processedTime', sizeForStruct(processedTime, resultSize), ... + 'vertexStart', sizeForStruct(vertexStart, resultSize), ... + 'vertexEnd', sizeForStruct(vertexEnd, resultSize), ... + 'fragmentStart', sizeForStruct(fragmentStart, resultSize), ... + 'fragmentEnd', sizeForStruct(fragmentEnd, resultSize), ... + 'drawableAcquired', sizeForStruct(drawableAcquired, resultSize), ... + 'drawablePresented', sizeForStruct(drawablePresented, resultSize)); + +% Convert x to something we can pass to struct(): +% - a cell array of the expected size +% - a scalar that can be automatically expanded +function x = sizeForStruct(x, resultSize) +if numel(x) == 1 + return +end +x = num2cell(reshape(x, resultSize)); \ No newline at end of file diff --git a/mgllib/mglSocket/mglSocketAcceptConnection.mexmaca64 b/mgllib/mglSocket/mglSocketAcceptConnection.mexmaca64 index 2f71692e..4d5031b1 100755 Binary files a/mgllib/mglSocket/mglSocketAcceptConnection.mexmaca64 and b/mgllib/mglSocket/mglSocketAcceptConnection.mexmaca64 differ diff --git a/mgllib/mglSocket/mglSocketClose.mexmaca64 b/mgllib/mglSocket/mglSocketClose.mexmaca64 index 0a958fbc..41c60000 100755 Binary files a/mgllib/mglSocket/mglSocketClose.mexmaca64 and b/mgllib/mglSocket/mglSocketClose.mexmaca64 differ diff --git a/mgllib/mglSocket/mglSocketCommandTypes.mexmaca64 b/mgllib/mglSocket/mglSocketCommandTypes.mexmaca64 index e5abfce7..8b460bff 100755 Binary files a/mgllib/mglSocket/mglSocketCommandTypes.mexmaca64 and b/mgllib/mglSocket/mglSocketCommandTypes.mexmaca64 differ diff --git a/mgllib/mglSocket/mglSocketCreateClient.mexmaca64 b/mgllib/mglSocket/mglSocketCreateClient.mexmaca64 index 088dd5b8..97dff463 100755 Binary files a/mgllib/mglSocket/mglSocketCreateClient.mexmaca64 and b/mgllib/mglSocket/mglSocketCreateClient.mexmaca64 differ diff --git a/mgllib/mglSocket/mglSocketCreateServer.mexmaca64 b/mgllib/mglSocket/mglSocketCreateServer.mexmaca64 index 9c677a6b..50b198e3 100755 Binary files a/mgllib/mglSocket/mglSocketCreateServer.mexmaca64 and b/mgllib/mglSocket/mglSocketCreateServer.mexmaca64 differ diff --git a/mgllib/mglSocket/mglSocketDataWaiting.mexmaca64 b/mgllib/mglSocket/mglSocketDataWaiting.mexmaca64 index 61448f66..2263f270 100755 Binary files a/mgllib/mglSocket/mglSocketDataWaiting.mexmaca64 and b/mgllib/mglSocket/mglSocketDataWaiting.mexmaca64 differ diff --git a/mgllib/mglSocket/mglSocketRead.mexmaca64 b/mgllib/mglSocket/mglSocketRead.mexmaca64 index ed031874..42b05084 100755 Binary files a/mgllib/mglSocket/mglSocketRead.mexmaca64 and b/mgllib/mglSocket/mglSocketRead.mexmaca64 differ diff --git a/mgllib/mglSocket/mglSocketWrite.mexmaca64 b/mgllib/mglSocket/mglSocketWrite.mexmaca64 index 9c253531..8b631a10 100755 Binary files a/mgllib/mglSocket/mglSocketWrite.mexmaca64 and b/mgllib/mglSocket/mglSocketWrite.mexmaca64 differ diff --git a/mgllib/mglStencilCreateBegin.m b/mgllib/mglStencilCreateBegin.m index 72e814ac..0d6655c0 100644 --- a/mgllib/mglStencilCreateBegin.m +++ b/mgllib/mglStencilCreateBegin.m @@ -1,7 +1,7 @@ % mglStencilCreateBegin % % $Id$ -% usage: [ackTime, processedTime] = mglStencilCreateBegin(stencilNumber, invert, socketInfo) +% usage: results = mglStencilCreateBegin(stencilNumber, invert, socketInfo) % by: justin gardner and ben heasly % date: 05/26/2006 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -26,7 +26,7 @@ %mglPoints2(rand(1,5000)*500,rand(1,5000)*500); %mglFlush; %mglStencilSelect(0); -function [ackTime, processedTime] = mglStencilCreateBegin(stencilNumber, invert, socketInfo) +function results = mglStencilCreateBegin(stencilNumber, invert, socketInfo) if nargin < 2 invert = 0; @@ -56,12 +56,12 @@ mglStencilCreateEnd(socketInfo); % Now let the caller draw into the requested stencil plane. -[ackTime, processedTime] = startStencilCreation(stencilNumber, invert, socketInfo); +results = startStencilCreation(stencilNumber, invert, socketInfo); -function [ackTime, processedTime] = startStencilCreation(stencilNumber, invert, socketInfo) +function results = startStencilCreation(stencilNumber, invert, socketInfo) mglSocketWrite(socketInfo, socketInfo(1).command.mglStartStencilCreation); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(stencilNumber)); mglSocketWrite(socketInfo, uint32(invert)); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglStencilCreateEnd.m b/mgllib/mglStencilCreateEnd.m index 856d83e9..f67789c6 100644 --- a/mgllib/mglStencilCreateEnd.m +++ b/mgllib/mglStencilCreateEnd.m @@ -1,7 +1,7 @@ % mglStencilCreateEnd % % $Id$ -% usage: [ackTime, processedTime] = mglStencilCreateEnd(socketInfo) +% usage: results = mglStencilCreateEnd(socketInfo) % by: justin gardner % date: 05/26/2006 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -22,7 +22,7 @@ %mglPoints2(rand(1,5000)*500,rand(1,5000)*500); %mglFlush; %mglStencilSelect(0); -function [ackTime, processedTime] = mglStencilCreateEnd(socketInfo) +function results = mglStencilCreateEnd(socketInfo) if nargin < 1 || isempty(socketInfo) global mgl @@ -36,4 +36,5 @@ % Get ready for regular drawign with no stencil selected. mglSocketWrite(socketInfo, socketInfo(1).command.mglFinishStencilCreation); ackTime = mglSocketRead(socketInfo, 'double'); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); + diff --git a/mgllib/mglStencilSelect.m b/mgllib/mglStencilSelect.m index 3f95ff83..6e291b46 100644 --- a/mgllib/mglStencilSelect.m +++ b/mgllib/mglStencilSelect.m @@ -1,7 +1,7 @@ % mglStencilSelect % % $Id$ -% usage: [ackTime, processedTime] = mglStencilSelect(stencilNumber) +% usage: results = mglStencilSelect(stencilNumber) % by: justin gardner and ben heasly % date: 05/26/2006 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -22,7 +22,7 @@ %mglPoints2(rand(1,5000)*500,rand(1,5000)*500); %mglFlush; %mglStencilSelect(0); -function [ackTime, processedTime] = mglStencilSelect(stencilNumber, socketInfo) +function results = mglStencilSelect(stencilNumber, socketInfo) if nargin < 2 || isempty(socketInfo) global mgl @@ -32,4 +32,4 @@ mglSocketWrite(socketInfo, socketInfo(1).command.mglSelectStencil); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(stencilNumber)); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglTest/mglTestStencil.m b/mgllib/mglTest/mglTestStencil.m index 9cf2455f..1fa250b8 100644 --- a/mgllib/mglTest/mglTestStencil.m +++ b/mgllib/mglTest/mglTestStencil.m @@ -30,14 +30,14 @@ function mglTestStencil(screenNumber) mglClose; return end -mglFillOval(0,0,[5*mglGetParam('xDeviceToPixels') 4*mglGetParam('yDeviceToPixels')]); +mglFillOval(0,0,[5 4]); mglStencilCreateEnd; mglClearScreen; mglFlush % Draw an oval stencil mglStencilCreateBegin(2,1); -mglFillOval(0,0,[8*mglGetParam('xDeviceToPixels') 8*mglGetParam('yDeviceToPixels')]); +mglFillOval(0,0,[8 8]); mglStencilCreateEnd; mglClearScreen; diff --git a/mgllib/mglTestMetal/mglPlotFrameTimes.m b/mgllib/mglTestMetal/mglPlotFrameTimes.m deleted file mode 100644 index b42a5ec8..00000000 --- a/mgllib/mglTestMetal/mglPlotFrameTimes.m +++ /dev/null @@ -1,40 +0,0 @@ -% mglPlotFrameTimes: visualize a sequence of frame times from mglFlush. -% -% usage: mglPlotFrameTimes(ackTimes, processedTimes) -% by: Benjamin Heasly -% date: 03/10/2022 -% copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) -% purpose: Visualize a sequence of frame times. -% usage: mglPlotFrameTimes(times) -% -function mglPlotFrameTimes(ackTimes, processedTimes, name, refreshRate) - -if nargin < 3 - name = 'Frame Times'; -end - -if nargin < 4 - displays = mglDescribeDisplays(); - refreshRate = displays(1).refreshRate; -end -expectedFrameTime = 1 / refreshRate; - -% For mglFlush(), "ackTime" is when the flush command was received, -% which is an indication of when we finished sending draw commands. -% mglFlush() then waits until "processedTime", the start of the next frame. -% From one processed to the next ack: how long we spend drawing stuff. -% From one processed to the next processed: how long the frame was. -drawingTimes = ackTimes(2:end) - processedTimes(1:end-1); -frameTimes = processedTimes(2:end) - processedTimes(1:end-1); - -figure('Name', name); -xAxis = 1:numel(drawingTimes); -line(xAxis, drawingTimes, 'Marker', '.', 'LineStyle', 'none', 'Color', 'red'); -line(xAxis, frameTimes, 'Marker', '*', 'LineStyle', 'none', 'Color', 'blue'); -grid('on'); -yticks(expectedFrameTime * (0:5)); -ylim([0, expectedFrameTime * 5]); -legend('draw', 'flip'); -title(name); -xlabel('frame number'); -ylabel('seconds'); \ No newline at end of file diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestFillOval.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestFillOval.m index 3ab1024e..3899cdfc 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestFillOval.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestFillOval.m @@ -29,10 +29,10 @@ function mglTestFillOval(isInteractive) x = [-5 -6 -3 4 5]; y = [ 5 1 -4 -2 3]; -mglFillOval(x, y, [4, 3], [1 .25 .25], 5); +mglFillOval(x, y, [2, 1.5], [1 .25 .25], .1); disp('There should be 5 wide, red ovals clustered roughly near the center.') -mglFillOval(x, y, [1, 2], [.25 .5 .75], 5); +mglFillOval(x, y, [0.5, 1], [.25 .5 .75], .1); disp('Each red oval should have a tall, blue-gray oval in the middle.') mglFlush(); diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestGlassDots.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestGlassDots.m index fd3a21d9..1db66d89 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestGlassDots.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestGlassDots.m @@ -47,8 +47,8 @@ function mglTestGlassDots(isInteractive) mglFlush(); if (isInteractive) - input('Hit ENTER to test moving dots (fullscreen): '); mglFlush(); + input('Hit ENTER to test moving dots (fullscreen): '); mglMetalFullscreen; mglPause(0.5) @@ -56,16 +56,16 @@ function mglTestGlassDots(isInteractive) deltaR = 0.005; deltaTheta = 0.03; nFrames = 1000; - ackTimes = zeros(1,nFrames); - processedTimes = zeros(1,nFrames); r = 0.25*rand(1,nVertices)+0.01; theta = rand(1,nVertices)*2*pi; x = cos(theta).*r; y = sin(theta).*r; + draws = cell(1, nFrames); + flushes = cell(1, nFrames); for iFrame = 1:nFrames - mglPoints2(x,y,0.01,[1 1 1]); - [ackTimes(iFrame), processedTimes(iFrame)] = mglFlush(); + draws{iFrame} = mglPoints2(x,y,0.01,[1 1 1]); + flushes{iFrame} = mglFlush(); r = r + deltaR; theta = theta + deltaTheta; @@ -74,6 +74,5 @@ function mglTestGlassDots(isInteractive) x = cos(theta).*r; y = sin(theta).*r; end - - mglPlotFrameTimes(ackTimes, processedTimes, 'moving dots'); + mglPlotCommandResults(flushes, draws, 'moving dots'); end diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestGluDisk.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestGluDisk.m index eaa24fd2..6d5ea5d1 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestGluDisk.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestGluDisk.m @@ -30,7 +30,7 @@ function mglTestGluDisk(isInteractive) nDisks = 5; x = linspace(-5, 5, nDisks); y = linspace(5, -5, nDisks); -size = 1; +size = 0.5; color = [0.25, 0.5, 0.75, 1.0]; mglGluDisk(x, y, size, color, 'ignored', 'ignored', 0); disp('There shoud be 5 disks across the diagonal.') diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalBatch.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalBatch.m new file mode 100644 index 00000000..59a6c02e --- /dev/null +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalBatch.m @@ -0,0 +1,71 @@ +% mglTestMetalBatch: an automated and/or interactive test for rendering. +% +% usage: mglTestMetalBatch(isInteractive) +% by: Benjamin Heasly +% date: 12 Jan 2024 +% copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) +% purpose: Test multiple flushes, repeated by the Metal app. +% usage: +% % You can run it by hand with no args. +% mglTestMetalBatch(); +% +% % Or mglRunRenderingTests can run it, in non-interactive mode. +% mglTestMetalBatch(false); +% +function mglTestMetalBatch(isInteractive) + +if nargin < 1 + isInteractive = true; +end + +if (isInteractive) + mglOpen(); + cleanup = onCleanup(@() mglClose()); +end + +%% How to: + +% Start a new batch. +% In this state the Mgl Metal app will accept commands to process later. +batchInfo = mglMetalStartBatch(); + +% Enqueue several drawing and non-drawing commands. +mglVisualAngleCoordinates(50, [20, 20]); +mglClearScreen([.25 1 .25]); + +% During a batch we won't see results from getter commands that return +% their own results. We'll only see placeholder results that prevent +% Matlab from blocking. These commands should still execute and not +% interfere with other commands in the batch. +[displayNumber, rect] = mglMetalGetWindowFrameInDisplay(); +assert(displayNumber == 0, "Should get placeholder response for display number.") +assert(isequal(rect, [0 0 0 0]), "Should get placeholder response for window rect.") + +% Enqueue an animation over several frames. +xSweep = linspace(-10, 10, 100); +for x = xSweep + mglPolygon(x + [-5 -6 -3 4 5], [5 1 -4 -2 3], [.25 .25 1]); + mglFillOval(x, 0, [3, 4], [1 .25 .25], 0.1); + mglFlush(); +end + +% Process the batch -- nothing will appear until we do this. +% In this state the Mgl Metal app will process commands enqueued above. +batchInfo = mglMetalProcessBatch(batchInfo); + +disp('Mgl Metal is executing commands asynchronously.'); + +% Wait for the commands to finish, and gather the timing results. +results = mglMetalFinishBatch(batchInfo); +commandCount = 4 + 1 + 1 + 3 * numel(xSweep); +assert(numel(results) == commandCount, 'Number of batched command results must equal number of batched commands.'); + +disp('A red oval and blue polygon should sweep over a green background.'); + +if (isInteractive) + codes = mglSocketCommandTypes(); + isPolygon = [results.commandCode] == codes.mglPolygon; + isFlush = [results.commandCode] == codes.mglFlush; + mglPlotCommandResults(results(isFlush), results(isPolygon), "Batch"); + mglPause(); +end diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingBlts.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingBlts.m index 82dc7781..207e0add 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingBlts.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingBlts.m @@ -29,15 +29,32 @@ function mglTestMetalRepeatingBlts(isInteractive) % Set a consistent rng state so they come out the same each time. rng(4242, 'twister'); nTextures = 5; +textures = cell(1, nTextures); for ii = 1:nTextures - textures(ii) = mglCreateTexture(rand(500, 500, 4)); + % Choose "nearest" sample filtering and "repeat" address mode. + texture = mglCreateTexture(rand(500, 500, 4)); + texture.minMagFilter = 0; + texture.mipFilter = 1; + texture.addressMode = 2; + textures{ii} = texture; end -% Blt those textures sequentually, frame by frame. -nFrames = 30; -mglMetalRepeatingBlts(nFrames); +% Enqueue blits of the textures sequentually, frame by frame. +nFrames = 32; +mglMetalStartBatch(); +for ii = 1 + mod(0:nFrames-1, nTextures) + mglBltTexture(textures{ii}, [0 0 2 2]); + mglFlush(); +end + +% Start processing the commands as fast as possible. +mglMetalProcessBatch(); +disp('Mgl Metal is repeating blt and flush commands asynchronously.'); + +% Wait for the commands to finish and gather the timing results. +results = mglMetalFinishBatch(); +assert(numel(results) == 2 * nFrames, 'Number of batched command results must equal number of batched commands.'); -% mglMetalRepeatingBlts should flush all on its own. disp('Random noise textures should fill the screen. The last texture should come out the same every time.'); if (isInteractive) diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingDots.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingDots.m index 29c3efa7..e5680fc6 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingDots.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingDots.m @@ -25,7 +25,7 @@ function mglTestMetalRepeatingDots(isInteractive) %% How to: -nFrames = 30; +nFrames = 31; nDots = 1000; randomSeed = 42; mglMetalRepeatingDots(nFrames, nDots, randomSeed); diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingFlicker.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingFlicker.m index 3e7f47b5..e60df9aa 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingFlicker.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingFlicker.m @@ -25,12 +25,12 @@ function mglTestMetalRepeatingFlicker(isInteractive) %% How to: -nFrames = 30; +nFrames = 31; randomSeed = 42; mglMetalRepeatingFlicker(nFrames, randomSeed); % mglMetalRepeatingFlicker should flush all on its own. -disp('Flicker for 30 frames with random seed 42 should leave us with a lavender clear color'); +disp('Flicker for 31 frames with random seed 42 should leave us with a lavender clear color'); % When it's done, make sure we have normal control again. % ie, calling flush shouldn't cause an error, get stuck, etc. diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingFlush.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingFlush.m index 081101d7..11695778 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingFlush.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingFlush.m @@ -25,11 +25,23 @@ function mglTestMetalRepeatingFlush(isInteractive) %% How to: +% Enqueue several flush commands without processing them yet. nFrames = 30; -mglMetalRepeatingFlush(nFrames); +mglMetalStartBatch(); +for ii = 1:nFrames + mglFlush(); +end + +% Start processing the commands as fast as possible. +mglMetalProcessBatch(); + +disp('Mgl Metal is repeating flush commands asynchronously.'); + +% Wait for the commands to finish and gather the timing results. +results = mglMetalFinishBatch(); +assert(numel(results) == nFrames, 'Number of batched command results must equal number of batched commands.'); -% mglMetalRepeatingFlush should flush all on its own. -disp('Flush for 30 frames should leave the screen blank'); +disp('Repeated flush commands should leave the screen blank.'); % When it's done, make sure we have normal control again. % ie, calling flush shouldn't cause an error, get stuck, etc. diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingQuads.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingQuads.m index a3a9c136..d6ef79c4 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingQuads.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalRepeatingQuads.m @@ -25,7 +25,7 @@ function mglTestMetalRepeatingQuads(isInteractive) %% How to: -nFrames = 30; +nFrames = 31; nQuads = 100; randomSeed = 42; mglMetalRepeatingQuads(nFrames, nQuads, randomSeed); diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalStencil.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalStencil.m index 627dad16..87fc1807 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalStencil.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestMetalStencil.m @@ -31,12 +31,12 @@ function mglTestMetalStencil(isInteractive) % Stencil 1 hides everything except a circle on the left side of the screen. mglStencilCreateBegin(1); -mglFillOval(-4, 0, [8, 8]); +mglFillOval(-4, 0, [4, 4]); mglStencilCreateEnd(); % Stencil 2 shows everything except a circle on the right side of the screen. mglStencilCreateBegin(2, 1); -mglFillOval(4, 0, [8, 8]); +mglFillOval(4, 0, [4, 4]); mglStencilCreateEnd(); % Stencil 3 is initialized to clear, hiding all regions. diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestQuads.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestQuads.m index e2b3fa21..b7de5d42 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestQuads.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestQuads.m @@ -53,25 +53,25 @@ function mglTestQuads(isInteractive) % Animate the checkers in a flickering pattern. nFlicker = 10; nFlush = 10; - ackTimes = zeros(1, 2*nFlush*nFlicker); - processedTimes = zeros(1, 2*nFlush*nFlicker); + nFrames = nFlicker + nFlush + nFlush; + draws = cell(1, nFrames); + flushes = cell(1, nFrames); iFrame = 1; for iFlicker = 1:nFlicker % Draw several frames of regular color. for iFlush = 1:nFlush - mglQuad(x,y,color); - [ackTimes(iFrame), processedTimes(iFrame)] = mglFlush(); + draws{iFrame} = mglQuad(x,y,color); + flushes{iFrame} = mglFlush(); iFrame = iFrame + 1; end % Draw several frames of alternate color. for iFlush = 1:nFlush % draw the quads - mglQuad(x,y,inverseColor); - [ackTimes(iFrame), processedTimes(iFrame)] = mglFlush(); + draws{iFrame} = mglQuad(x,y,inverseColor); + flushes{iFrame} = mglFlush(); iFrame = iFrame + 1; end end - - mglPlotFrameTimes(ackTimes, processedTimes, 'Flickering quads'); + mglPlotCommandResults(flushes, draws, 'Flickering quads'); end diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestTexture.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestTexture.m index c956c512..007c7322 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestTexture.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestTexture.m @@ -49,34 +49,34 @@ function mglTestTexture(isInteractive) if (isInteractive) disp('Hit ENTER to test blt with drifting phase (fullscreen): ');mglPause; - mglFlush(); mglMetalFullscreen(); mglPause(0.5); + mglFlush(); nFrames = 300; - ackTimes = zeros(1,nFrames); - processedTimes = zeros(1,nFrames); + draws = cell(1, nFrames); + flushes = cell(1, nFrames); phase = 0; for iFrame = 1:nFrames - mglMetalBltTexture(tex,[0 0],0,0,0,phase); + draws{iFrame} = mglMetalBltTexture(tex,[0 0],0,0,0,phase); phase = phase + (1/60)/4; - [ackTimes(iFrame), processedTimes(iFrame)] = mglFlush(); + flushes{iFrame} = mglFlush(); end mglMetalFullscreen(false); - mglPlotFrameTimes(ackTimes, processedTimes, 'blt with drifting phase'); + mglPlotCommandResults(flushes, draws, 'blt with drifting phase'); disp('Hit any key to test multiple blt with rotation and drifting phase (fullscreen): '); mglPause; - mglFlush(); mglMetalFullscreen(); mglPause(0.5); + mglFlush(); nFrames = 300; - ackTimes = zeros(1,nFrames); - processedTimes = zeros(1,nFrames); + draws = cell(1, nFrames); + flushes = cell(1, nFrames); phase = 0;rotation = 0;width = 0.25;height = 0.25; for iFrame = 1:nFrames - mglMetalBltTexture(tex,[-0.5 -0.5],0,0,rotation,phase,width,height); + draws{iFrame} = mglMetalBltTexture(tex,[-0.5 -0.5],0,0,rotation,phase,width,height); mglMetalBltTexture(tex,[-0.5 0.5],0,0,-rotation,phase,width,height); mglMetalBltTexture(tex,[0.5 0.5],0,0,rotation,phase,width,height); mglMetalBltTexture(tex,[0.5 -0.5],0,0,-rotation,phase,width,height); @@ -92,10 +92,10 @@ function mglTestTexture(isInteractive) phase = phase + (1/60)/4; rotation = rotation+360/nFrames; - [ackTimes(iFrame), processedTimes(iFrame)] = mglFlush(); + flushes{iFrame} = mglFlush(); end mglMetalFullscreen(false); - mglPlotFrameTimes(ackTimes, processedTimes, 'blt with rotation and drifting phase'); + mglPlotCommandResults(flushes, draws, 'blt with rotation and drifting phase'); end % Delete will have little effect, since we're about to mglClose(). diff --git a/mgllib/mglTestMetal/mglRenderingTests/mglTestUpdateTexture.m b/mgllib/mglTestMetal/mglRenderingTests/mglTestUpdateTexture.m index 283a9144..1a2551ca 100644 --- a/mgllib/mglTestMetal/mglRenderingTests/mglTestUpdateTexture.m +++ b/mgllib/mglTestMetal/mglRenderingTests/mglTestUpdateTexture.m @@ -47,25 +47,24 @@ function mglTestUpdateTexture(isInteractive) if (isInteractive) input('Hit ENTER to test frame-by-frame texture updates (fullscreen): '); - mglFlush(); mglMetalFullscreen(); mglPause(0.5); + mglFlush(); nFrames = 300; - ackTimes = zeros(1,nFrames); - processedTimes = zeros(1,nFrames); + draws = cell(1, nFrames); + flushes = cell(1, nFrames); shiftedImage = newImage; for iFrame = 1:nFrames shiftedImage(:,:,1) = circshift(shiftedImage(:,:,1), 1); shiftedImage(:,:,2) = circshift(shiftedImage(:,:,2), 1); shiftedImage(:,:,3) = circshift(shiftedImage(:,:,3), 1); - mglUpdateTexture(tex, shiftedImage); + draws{iFrame} = mglUpdateTexture(tex, shiftedImage); mglMetalBltTexture(tex,[0 0],0,0,0,0,2,2); - [ackTimes(iFrame), processedTimes(iFrame)] = mglFlush(); + flushes{iFrame} = mglFlush(); end - mglMetalFullscreen(false); name = sprintf('frame-by-frame texture updates %d x %d', textureWidth, textureHeight); - mglPlotFrameTimes(ackTimes, processedTimes, name); + mglPlotCommandResults(flushes, draws, name); end % Delete will have little effect, since we're about to mglClose(). diff --git a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestFillOval.mat b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestFillOval.mat index 5033290d..31ea447d 100644 Binary files a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestFillOval.mat and b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestFillOval.mat differ diff --git a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestGluDisk.mat b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestGluDisk.mat index c1cd503b..c60d024a 100644 Binary files a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestGluDisk.mat and b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestGluDisk.mat differ diff --git a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalArcs.mat b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalArcs.mat index 6010c8fb..50179763 100644 Binary files a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalArcs.mat and b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalArcs.mat differ diff --git a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalBatch.mat b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalBatch.mat new file mode 100644 index 00000000..b05cb3b2 Binary files /dev/null and b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalBatch.mat differ diff --git a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalRepeatingBlts.mat b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalRepeatingBlts.mat index 1cea455c..b7fef3f2 100644 Binary files a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalRepeatingBlts.mat and b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalRepeatingBlts.mat differ diff --git a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalStencil.mat b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalStencil.mat index 2fb95380..a57f17f6 100644 Binary files a/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalStencil.mat and b/mgllib/mglTestMetal/mglRenderingTests/snapshots/mglTestMetalStencil.mat differ diff --git a/mgllib/mglTestMetal/mglRunRenderingTests.m b/mgllib/mglTestMetal/mglRunRenderingTests.m index cb5571af..8a9eb265 100644 --- a/mgllib/mglTestMetal/mglRunRenderingTests.m +++ b/mgllib/mglTestMetal/mglRunRenderingTests.m @@ -47,13 +47,13 @@ fprintf(' snapshot data: %s\n', result.snapshotData); expectedFigure = figure('Name', sprintf('%s Expected', result.testName)); expectedAxes = axes('Parent', expectedFigure); - imshow(result.snapshot(:,:,1:3), 'InitialMagnification', 100, 'Parent', expectedAxes); + imshow(result.snapshot(:,:,1:3), 'InitialMagnification', 200, 'Parent', expectedAxes); title(sprintf('Expected (%d x %d)', size(result.snapshot, 1), size(result.snapshot, 2)), 'Parent', expectedAxes); actualFigure = figure('Name', sprintf('%s Actual', result.testName)); actualAxes = axes('Parent', actualFigure); if size(result.renderedImage, 3) >= 3 - imshow(result.renderedImage(:,:,1:3), 'InitialMagnification', 100, 'Parent', actualAxes); + imshow(result.renderedImage(:,:,1:3), 'InitialMagnification', 200, 'Parent', actualAxes); end title(sprintf('Actual (%d x %d)', size(result.renderedImage, 1), size(result.renderedImage, 2)), 'Parent', actualAxes); end diff --git a/mgllib/mglTestRenderPipeline.m b/mgllib/mglTestRenderPipeline.m index ce073b81..c4f44e54 100644 --- a/mgllib/mglTestRenderPipeline.m +++ b/mgllib/mglTestRenderPipeline.m @@ -84,7 +84,7 @@ %%%%%%%%%%%%%% function d = parseArgs(args,d) -getArgs(args,{'screenNum=1','runTests=[]','testLen=5','dropThreshold=0.1','numQuads=250','numPoints=10000','bltSize=[]','numBlt=30','initWaitTime=1','n=[]','photoDiodeTest=0','photoDiodeRect=[-1 -1 0.5 0.5]','photoDiodeColor=[1 1 1]'}); +getArgs(args,{'screenNum=1','runTests=[]','testLen=5','dropThreshold=0.1','numQuads=250','numPoints=10000','bltSize=[]','numBlt=30','initWaitTime=1','n=[]','photoDiodeTest=0','photoDiodeRect=[-1 -1 0.5 0.5]','photoDiodeColor=[1 1 1]', 'batchMode=0'}); % set some parameters d.screenNum = screenNum; @@ -109,6 +109,9 @@ d.photoDiodeColor = photoDiodeColor; d.photoDiodeX = [d.photoDiodeRect(1) d.photoDiodeRect(1) d.photoDiodeRect(1)+d.photoDiodeRect(3) d.photoDiodeRect(1)+d.photoDiodeRect(3)]'; d.photoDiodeY = [d.photoDiodeRect(2) d.photoDiodeRect(2)+d.photoDiodeRect(4) d.photoDiodeRect(2)+d.photoDiodeRect(4) d.photoDiodeRect(2)]'; + +d.batchMode = batchMode; + %%%%%%%%%%%%%% % testFlush %%%%%%%%%%%%%% @@ -117,7 +120,12 @@ d.testName = 'Flush test'; disppercent(-inf,sprintf('(mglTestRenderPipeline) Testing flush. Please wait %0.1f secs',d.testLen)); +if d.batchMode + batchInfo = mglMetalStartBatch(); +end + % do the appropriate number of flush +flushes = cell(1, d.numFrames); for iFrame = 1:d.numFrames % get start time of frame d.timeVec(1,iFrame) = mglGetSecs; @@ -132,11 +140,24 @@ end % and flush - [d.timeVec(5,iFrame) d.timeVec(6,iFrame)] = mglFlush; + flushes{iFrame} = mglFlush(); + d.timeVec(5,iFrame) = flushes{iFrame}.ackTime; + d.timeVec(6,iFrame) = flushes{iFrame}.processedTime; % and record time d.timeVec(7,iFrame) = mglGetSecs; end +if d.batchMode + batchInfo = mglMetalProcessBatch(batchInfo); + commands = mglMetalFinishBatch(batchInfo); + codes = mglSocketCommandTypes(); + d.flushes = commands([commands.commandCode] == codes.mglFlush); + d.draws = []; +else + d.flushes = flushes; + d.draws = []; +end + disppercent(inf); %%%%%%%%%%%%%% @@ -147,23 +168,48 @@ d.testName = sprintf('%i Quads test',d.numQuads); disppercent(-inf,sprintf('(mglTestRenderPipeline) Testing quads. Please wait %0.1f secs',d.testLen)); +if d.batchMode + batchInfo = mglMetalStartBatch(); +end + % do the appropriate number of flush +draws = cell(1, d.numFrames); +flushes = cell(1, d.numFrames); for iFrame = 1:d.numFrames % get start time of frame d.timeVec(1,iFrame) = mglGetSecs; % draw quads - [d.timeVec(3,iFrame) d.timeVec(4,iFrame) d.timeVec(2,iFrame)] = mglQuad(2*rand(4,d.numQuads)-1,2*rand(4,d.numQuads)-1,rand(3,d.numQuads)); + draws{iFrame} = mglQuad(2*rand(4,d.numQuads)-1,2*rand(4,d.numQuads)-1,rand(3,d.numQuads)); + d.timeVec(3,iFrame) = draws{iFrame}.ackTime; + d.timeVec(4,iFrame) = draws{iFrame}.processedTime; + d.timeVec(2,iFrame) = draws{iFrame}.setupTime; % draw photoDiodeRect if need be if d.photoDiodeTest if iseven(iFrame), photoDiodeColor = d.photoDiodeColor; else photoDiodeColor = [0 0 0]; end mglQuad(d.photoDiodeX,d.photoDiodeY,photoDiodeColor); end % and flush - [d.timeVec(5,iFrame) d.timeVec(6,iFrame)] = mglFlush; + flushes{iFrame} = mglFlush(); + d.timeVec(5,iFrame) = flushes{iFrame}.ackTime; + d.timeVec(6,iFrame) = flushes{iFrame}.processedTime; % and record time d.timeVec(7,iFrame) = mglGetSecs; end +if d.batchMode + batchInfo = mglMetalProcessBatch(batchInfo); + commands = mglMetalFinishBatch(batchInfo); + codes = mglSocketCommandTypes(); + d.flushes = commands([commands.commandCode] == codes.mglFlush); + + % TODO: this would break if photoDiodeTest also draws quds. + % Add a correlation ID to commands? + d.draws = commands([commands.commandCode] == codes.mglQuad); +else + d.flushes = flushes; + d.draws = draws; +end + disppercent(inf); %%%%%%%%%%%%%% @@ -174,23 +220,45 @@ d.testName = sprintf('%i dots test',d.numPoints); disppercent(-inf,sprintf('(mglTestRenderPipeline) Testing dots. Please wait %0.1f secs',d.testLen)); +if d.batchMode + batchInfo = mglMetalStartBatch(); +end + % do the appropriate number of flush +draws = cell(1, d.numFrames); +flushes = cell(1, d.numFrames); for iFrame = 1:d.numFrames % get start time of frame d.timeVec(1,iFrame) = mglGetSecs; % draw dots - [d.timeVec(3,iFrame) d.timeVec(4,iFrame) d.timeVec(2,iFrame)] = mglPoints2c(2*rand(1,d.numPoints)-1,2*rand(1,d.numPoints)-1,0.005*ones(d.numPoints,2),rand(1,d.numPoints),rand(1,d.numPoints),rand(1,d.numPoints)); + draws{iFrame} = mglPoints2c(2*rand(1,d.numPoints)-1,2*rand(1,d.numPoints)-1,0.005*ones(d.numPoints,2),rand(1,d.numPoints),rand(1,d.numPoints),rand(1,d.numPoints)); + d.timeVec(3,iFrame) = draws{iFrame}.ackTime; + d.timeVec(4,iFrame) = draws{iFrame}.processedTime; + d.timeVec(2,iFrame) = draws{iFrame}.setupTime; % draw photoDiodeRect if need be if d.photoDiodeTest if iseven(iFrame), photoDiodeColor = d.photoDiodeColor; else photoDiodeColor = [0 0 0]; end mglQuad(d.photoDiodeX,d.photoDiodeY,photoDiodeColor); end % and flush - [d.timeVec(5,iFrame) d.timeVec(6,iFrame)] = mglFlush; + flushes{iFrame} = mglFlush(); + d.timeVec(5,iFrame) = flushes{iFrame}.ackTime; + d.timeVec(6,iFrame) = flushes{iFrame}.processedTime; % and record time d.timeVec(7,iFrame) = mglGetSecs; end +if d.batchMode + batchInfo = mglMetalProcessBatch(batchInfo); + commands = mglMetalFinishBatch(batchInfo); + codes = mglSocketCommandTypes(); + d.flushes = commands([commands.commandCode] == codes.mglFlush); + d.draws = commands([commands.commandCode] == codes.mglDots); +else + d.flushes = flushes; + d.draws = draws; +end + disppercent(inf); %%%%%%%%%%%%%% @@ -214,23 +282,45 @@ d.testName = sprintf('%ix%i n=%i Blt test',d.bltSize(1),d.bltSize(2),d.numBlt); disppercent(-inf,sprintf('(mglTestRenderPipeline) Testing %s. Please wait %0.1f secs',d.testName,d.testLen)); +if d.batchMode + batchInfo = mglMetalStartBatch(); +end + % do the appropriate number of flush +draws = cell(1, d.numFrames); +flushes = cell(1, d.numFrames); for iFrame = 1:d.numFrames % get start time of frame d.timeVec(1,iFrame) = mglGetSecs; % draw texture - [d.timeVec(3,iFrame) d.timeVec(4,iFrame) d.timeVec(2,iFrame)] = mglBltTexture(tex(mod(iFrame,d.numBlt)+1),[0 0 2 2]); + draws{iFrame} = mglBltTexture(tex(mod(iFrame,d.numBlt)+1),[0 0 2 2]); + d.timeVec(3,iFrame) = draws{iFrame}.ackTime; + d.timeVec(4,iFrame) = draws{iFrame}.processedTime; + d.timeVec(2,iFrame) = draws{iFrame}.setupTime; % draw photoDiodeRect if need be if d.photoDiodeTest if iseven(iFrame), photoDiodeColor = d.photoDiodeColor; else photoDiodeColor = [0 0 0]; end mglQuad(d.photoDiodeX,d.photoDiodeY,photoDiodeColor); end % and flush - [d.timeVec(5,iFrame) d.timeVec(6,iFrame)] = mglFlush; + flushes{iFrame} = mglFlush(); + d.timeVec(5,iFrame) = flushes{iFrame}.ackTime; + d.timeVec(6,iFrame) = flushes{iFrame}.processedTime; % and record time d.timeVec(7,iFrame) = mglGetSecs; end +if d.batchMode + batchInfo = mglMetalProcessBatch(batchInfo); + commands = mglMetalFinishBatch(batchInfo); + codes = mglSocketCommandTypes(); + d.flushes = commands([commands.commandCode] == codes.mglFlush); + d.draws = commands([commands.commandCode] == codes.mglBltTexture); +else + d.flushes = flushes; + d.draws = draws; +end + disppercent(inf); %%%%%%%%%%%%%% @@ -241,23 +331,45 @@ d.testName = sprintf('Clear screen test'); disppercent(-inf,sprintf('(mglTestRenderPipeline) Testing clear screen. Please wait %0.1f secs',d.testLen)); +if d.batchMode + batchInfo = mglMetalStartBatch(); +end + % do the appropriate number of flush +draws = cell(1, d.numFrames); +flushes = cell(1, d.numFrames); for iFrame = 1:d.numFrames % get start time of frame d.timeVec(1,iFrame) = mglGetSecs; % set the clear color - [d.timeVec(3,iFrame) d.timeVec(4,iFrame) d.timeVec(2,iFrame)] = mglClearScreen(rand(1,3)); + draws{iFrame} = mglClearScreen(rand(1,3)); + d.timeVec(3,iFrame) = draws{iFrame}.ackTime; + d.timeVec(4,iFrame) = draws{iFrame}.processedTime; + d.timeVec(2,iFrame) = draws{iFrame}.setupTime; % draw photoDiodeRect if need be if d.photoDiodeTest if iseven(iFrame), photoDiodeColor = d.photoDiodeColor; else photoDiodeColor = [0 0 0]; end mglQuad(d.photoDiodeX,d.photoDiodeY,photoDiodeColor); end % and flush - [d.timeVec(5,iFrame) d.timeVec(6,iFrame)] = mglFlush; + flushes{iFrame} = mglFlush(); + d.timeVec(5,iFrame) = flushes{iFrame}.ackTime; + d.timeVec(6,iFrame) = flushes{iFrame}.processedTime; % and record time d.timeVec(7,iFrame) = mglGetSecs; end +if d.batchMode + batchInfo = mglMetalProcessBatch(batchInfo); + commands = mglMetalFinishBatch(batchInfo); + codes = mglSocketCommandTypes(); + d.flushes = commands([commands.commandCode] == codes.mglFlush); + d.draws = commands([commands.commandCode] == codes.mglSetClearColor); +else + d.flushes = flushes; + d.draws = draws; +end + disppercent(inf); %%%%%%%%%%%%%%%% @@ -270,9 +382,13 @@ function dispTimeTest(d) % plot the data plot(1000*(d.timeVec(2,:)-d.timeVec(1,:)),'c.'); hold on plot(1000*(d.timeVec(3,:)-d.timeVec(1,:)),'k.'); -plot(1000*(d.timeVec(4,:)-d.timeVec(1,:)),'g.'); +if ~all([d.timeVec(4,:)] == 0) + plot(1000*(d.timeVec(4,:)-d.timeVec(1,:)),'g.'); +end plot(1000*(d.timeVec(5,:)-d.timeVec(1,:)),'b.'); -plot(1000*(d.timeVec(6,:)-d.timeVec(1,:)),'bo'); +if ~all([d.timeVec(4,:)] == 0) + plot(1000*(d.timeVec(6,:)-d.timeVec(1,:)),'bo'); +end plot(1000*(d.timeVec(end,:)-d.timeVec(1,:)),'r.'); hline(1000/d.frameRate,'r'); hline((1+d.dropThreshold)*(1000/d.frameRate),'r:'); @@ -287,3 +403,12 @@ function dispTimeTest(d) title(sprintf('(%s) %i fames took longer than %0.1f%% than expected at %i Hz',d.testName,dropFrames,100*d.dropThreshold,d.frameRate)); legend('Matlab setup','Draw commands Ack','Draw commands Processed','Flush Ack','Flush processed','End of frame loop'); + +% Plot sub-frame timestamps, too -- which is relatively new. +% There might be a good way to consolidate this new plot with the existing. +if d.batchMode + figureName = [d.testName ' (batch)']; +else + figureName = [d.testName ' (interactive)']; +end +mglPlotCommandResults(d.flushes, d.draws, figureName); diff --git a/mgllib/mglText.m b/mgllib/mglText.m index 5a07d16c..a9cc44e0 100644 --- a/mgllib/mglText.m +++ b/mgllib/mglText.m @@ -1,7 +1,7 @@ % mglText.m % % $Id$ -% usage: [tex, ackTime, processedTime] = mglText('string') +% usage: [tex, results] = mglText('string') % by: justin gardner % date: 09/28/2021 Based on version from 05/10/06 % copyright: (c) 2021 Justin Gardner (GPL see mgl/COPYING) @@ -15,7 +15,7 @@ %thisText = mglText('Hello') %mglBltTexture(thisText,[0 0],'left','top'); %mglFlush; -function [tex, ackTime, processedTime] = mglText(str) +function [tex, results] = mglText(str) if (nargin ~= 1) || ~isstr(str) help mglText; @@ -52,4 +52,4 @@ %im.textImage(:,:,4) = 255*(im.textImage(:,:,1)>0 | im.textImage(:,:,2)>0 | im.textImage(:,:,3)>0); % create the texture -[tex, ackTime, processedTime] = mglCreateTexture(im.textImage); +[tex, results] = mglCreateTexture(im.textImage); diff --git a/mgllib/mglTransform.m b/mgllib/mglTransform.m index 1193d14b..16255e05 100644 --- a/mgllib/mglTransform.m +++ b/mgllib/mglTransform.m @@ -1,5 +1,5 @@ % $Id$ -% usage: [currentMatrix, ackTime, processedTime] = mglTransform(operation, value, socketInfo) +% usage: [currentMatrix, results] = mglTransform(operation, value, socketInfo) % by: Ben Heasly % date: 2021-12-03 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -17,10 +17,9 @@ % This function is usually not called directly, but % called by mglVisualAngleCoordinates or % mglScreenCoordinates to set the transforms -function [currentMatrix, ackTime, processedTime] = mglTransform(operation, value, socketInfo) +function [currentMatrix, results] = mglTransform(operation, value, socketInfo) -ackTime = []; -processedTime = []; +results = []; persistent mglTransformIsNotOpenGLAnymore if isempty(mglTransformIsNotOpenGLAnymore) @@ -58,4 +57,4 @@ mglSocketWrite(socketInfo, socketInfo(1).command.mglSetXform); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, single(mgl.currentMatrix)); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime); diff --git a/mgllib/mglUpdateTexture.m b/mgllib/mglUpdateTexture.m index af6a286b..961cd746 100644 --- a/mgllib/mglUpdateTexture.m +++ b/mgllib/mglUpdateTexture.m @@ -1,7 +1,7 @@ % mglUpdateTexture.m % % $Id$ -% usage: [ackTime, processedTime] = mglUpdateTexture(texture, image) +% usage: results = mglUpdateTexture(texture, image) % by: Benjamin Heasly % date: 03/18/2022 % copyright: (c) 2006 Justin Gardner, Jonas Larsson (GPL see mgl/COPYING) @@ -36,7 +36,7 @@ % mglBltTexture(tex,[0 0]); % mglFlush(); % -function [ackTime, processedTime] = mglUpdateTexture(tex, im, socketInfo) +function results = mglUpdateTexture(tex, im, socketInfo) if numel(tex) > 1 fprintf('(mglUpdateTexture) Only using the first of %d elements of tex struct array. To avoid this warning pass in tex(1) instead.\n', numel(tex)); @@ -65,10 +65,13 @@ % mglMetalCreateTexture has additional commentary on this! im = permute(im, [3,2,1]); +setupTime = mglGetSecs(); + mglSocketWrite(socketInfo, socketInfo(1).command.mglUpdateTexture); ackTime = mglSocketRead(socketInfo, 'double'); mglSocketWrite(socketInfo, uint32(tex.textureNumber)); mglSocketWrite(socketInfo, uint32(newWidth)); mglSocketWrite(socketInfo, uint32(newHeight)); mglSocketWrite(socketInfo, single(im(:))); -processedTime = mglSocketRead(socketInfo, 'double'); +results = mglReadCommandResults(socketInfo, ackTime, setupTime); +