Repository: linebender/piet-metal Branch: master Commit: 71afb99a68f8 Files: 32 Total size: 214.4 KB Directory structure: gitextract_b08e_b5r/ ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── TestApp/ │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── Main.storyboard │ ├── GenTypes.h │ ├── Info.plist │ ├── PietRender.metal │ ├── PietRenderer.h │ ├── PietRenderer.m │ ├── PietShaderTypes.h │ ├── SceneEncoder.h │ ├── SceneEncoder.m │ ├── ViewController.h │ ├── ViewController.m │ ├── main.m │ └── piet_metal.entitlements ├── include/ │ └── piet_metal.h ├── piet-gpu-derive/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── piet-metal.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata/ │ └── raph.xcuserdatad/ │ └── xcschemes/ │ └── xcschememanagement.plist └── src/ ├── flatten.rs ├── lib.rs └── main.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /target /build .idea ================================================ FILE: Cargo.toml ================================================ [package] name = "piet-metal" version = "0.0.0" authors = ["Raph Levien "] description = "A Metal-based renderer for the piet 2D graphics abstraction." license = "MIT/Apache-2.0" edition = "2018" keywords = ["graphics", "2d"] categories = ["rendering::graphics-api"] [dependencies] kurbo = "0.5.6" piet-gpu-derive = { path = "./piet-gpu-derive" } # this is for reading the tiger, will be factored out roxmltree = "0.6.0" [lib] name = "piet_metal" crate-type = ["staticlib"] ================================================ FILE: LICENSE-APACHE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: LICENSE-MIT ================================================ Copyright (c) 2019 Raph Levien Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # piet-metal This repository is currently an experiment in using GPU compute to implement the piet 2D graphics API. In its initial stages it is an Objective-C macOS application, to make it easier to use Xcode tools. When it becomes more functional, Rust bindings will be added, with the Objective-C code for the Metal bindings built from the Rust library's build.rs. ## Following along I'm discussing the design and my findings in the [#druid] stream on xi Zulip. I've also creates a [notes document]. ## License Licensed under either of these: * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) [notes document]: https://docs.google.com/document/d/1LILagXyJgYtlm6y83x1Mc2VoNfOcvW_ZiCldZbs4yO8/edit?usp=sharing [#druid]: https://xi.zulipchat.com/#narrow/stream/147926-druid ================================================ FILE: TestApp/AppDelegate.h ================================================ // Copyright 2019 The xi-editor authors. #import @interface AppDelegate : NSObject @end ================================================ FILE: TestApp/AppDelegate.m ================================================ // Copyright 2019 The xi-editor authors. #import "AppDelegate.h" @interface AppDelegate () @end @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Insert code here to initialize your application } - (void)applicationWillTerminate:(NSNotification *)aNotification { // Insert code here to tear down your application } @end ================================================ FILE: TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "size" : "16x16", "scale" : "1x" }, { "idiom" : "mac", "size" : "16x16", "scale" : "2x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "1x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "2x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "1x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "2x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "1x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "2x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "1x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: TestApp/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: TestApp/Base.lproj/Main.storyboard ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: TestApp/GenTypes.h ================================================ // Copyright 2019 The xi-editor authors. // The contents of this file are autogenerated. // scene from new (merged) generator inline uint extract_8bit_value(uint bit_shift, uint package) { uint mask = 255; uint result = (package >> bit_shift) & mask; return result; } inline uint extract_16bit_value(uint bit_shift, uint package) { uint mask = 65535; uint result = (package >> bit_shift) & mask; return result; } typedef uint SimpleGroupRef; typedef uint PietCircleRef; typedef uint PietStrokeLineRef; typedef uint PietFillRef; typedef uint PietStrokePolyLineRef; typedef uint PietItemRef; struct SimpleGroupPacked { uint n_items; PietItemRef items_ix; uint2 bbox; }; inline SimpleGroupPacked SimpleGroup_read(const device char *buf, SimpleGroupRef ref) { SimpleGroupPacked result; uint n_items = *(device const uint*)(buf + ref); result.n_items = n_items; PietItemRef items_ix = *(device const uint*)(buf + ref + 4); result.items_ix = items_ix; uint2 bbox = *(device const packed_uint2*)(buf + ref + 8); result.bbox = bbox; return result; } inline uint SimpleGroup_n_items(const device char *buf, SimpleGroupRef ref) { uint n_items = *(device const uint*)(buf + ref); return n_items; } inline PietItemRef SimpleGroup_items_ix(const device char *buf, SimpleGroupRef ref) { PietItemRef items_ix = *(device const uint*)(buf + ref + 4); return items_ix; } inline uint2 SimpleGroup_bbox(const device char *buf, SimpleGroupRef ref) { uint2 bbox = *(device const packed_uint2*)(buf + ref + 8); return bbox; } inline uint4 SimpleGroup_unpack_bbox(uint2 bbox) { uint4 result; result[0] = extract_16bit_value(0, bbox[0]); result[1] = extract_16bit_value(16, bbox[0]); result[2] = extract_16bit_value(0, bbox[1]); result[3] = extract_16bit_value(16, bbox[1]); return result; } struct SimpleGroup { uint n_items; PietItemRef items_ix; uint4 bbox; }; inline SimpleGroup SimpleGroup_unpack(SimpleGroupPacked packed_form) { SimpleGroup result; result.n_items = packed_form.n_items; result.items_ix = packed_form.items_ix; result.bbox = SimpleGroup_unpack_bbox(packed_form.bbox); return result; } struct PietCirclePacked { uint tag; }; inline PietCirclePacked PietCircle_read(const device char *buf, PietCircleRef ref) { PietCirclePacked result; return result; } struct PietCircle { }; inline PietCircle PietCircle_unpack(PietCirclePacked packed_form) { PietCircle result; return result; } struct PietStrokeLinePacked { uint tag; uint flags; uint rgba_color; float width; float2 start; float2 end; }; inline PietStrokeLinePacked PietStrokeLine_read(const device char *buf, PietStrokeLineRef ref) { PietStrokeLinePacked result; uint flags = *(device const uint*)(buf + ref + 4); result.flags = flags; uint rgba_color = *(device const uint*)(buf + ref + 8); result.rgba_color = rgba_color; float width = as_type(*(device const uint*)(buf + ref + 12)); result.width = width; float2 start = as_type(*(device const packed_uint2*)(buf + ref + 16)); result.start = start; float2 end = as_type(*(device const packed_uint2*)(buf + ref + 24)); result.end = end; return result; } inline uint PietStrokeLine_flags(const device char *buf, PietStrokeLineRef ref) { uint flags = *(device const uint*)(buf + ref + 4); return flags; } inline uint PietStrokeLine_rgba_color(const device char *buf, PietStrokeLineRef ref) { uint rgba_color = *(device const uint*)(buf + ref + 8); return rgba_color; } inline float PietStrokeLine_width(const device char *buf, PietStrokeLineRef ref) { float width = as_type(*(device const uint*)(buf + ref + 12)); return width; } inline float2 PietStrokeLine_start(const device char *buf, PietStrokeLineRef ref) { float2 start = as_type(*(device const packed_uint2*)(buf + ref + 16)); return start; } inline float2 PietStrokeLine_end(const device char *buf, PietStrokeLineRef ref) { float2 end = as_type(*(device const packed_uint2*)(buf + ref + 24)); return end; } struct PietStrokeLine { uint flags; uint rgba_color; float width; float2 start; float2 end; }; inline PietStrokeLine PietStrokeLine_unpack(PietStrokeLinePacked packed_form) { PietStrokeLine result; result.flags = packed_form.flags; result.rgba_color = packed_form.rgba_color; result.width = packed_form.width; result.start = packed_form.start; result.end = packed_form.end; return result; } struct PietFillPacked { uint tag; uint flags; uint rgba_color; uint n_points; uint points_ix; }; inline PietFillPacked PietFill_read(const device char *buf, PietFillRef ref) { PietFillPacked result; uint flags = *(device const uint*)(buf + ref + 4); result.flags = flags; uint rgba_color = *(device const uint*)(buf + ref + 8); result.rgba_color = rgba_color; uint n_points = *(device const uint*)(buf + ref + 12); result.n_points = n_points; uint points_ix = *(device const uint*)(buf + ref + 16); result.points_ix = points_ix; return result; } inline uint PietFill_flags(const device char *buf, PietFillRef ref) { uint flags = *(device const uint*)(buf + ref + 4); return flags; } inline uint PietFill_rgba_color(const device char *buf, PietFillRef ref) { uint rgba_color = *(device const uint*)(buf + ref + 8); return rgba_color; } inline uint PietFill_n_points(const device char *buf, PietFillRef ref) { uint n_points = *(device const uint*)(buf + ref + 12); return n_points; } inline uint PietFill_points_ix(const device char *buf, PietFillRef ref) { uint points_ix = *(device const uint*)(buf + ref + 16); return points_ix; } struct PietFill { uint flags; uint rgba_color; uint n_points; uint points_ix; }; inline PietFill PietFill_unpack(PietFillPacked packed_form) { PietFill result; result.flags = packed_form.flags; result.rgba_color = packed_form.rgba_color; result.n_points = packed_form.n_points; result.points_ix = packed_form.points_ix; return result; } struct PietStrokePolyLinePacked { uint tag; uint rgba_color; float width; uint n_points; uint points_ix; }; inline PietStrokePolyLinePacked PietStrokePolyLine_read(const device char *buf, PietStrokePolyLineRef ref) { PietStrokePolyLinePacked result; uint rgba_color = *(device const uint*)(buf + ref + 4); result.rgba_color = rgba_color; float width = as_type(*(device const uint*)(buf + ref + 8)); result.width = width; uint n_points = *(device const uint*)(buf + ref + 12); result.n_points = n_points; uint points_ix = *(device const uint*)(buf + ref + 16); result.points_ix = points_ix; return result; } inline uint PietStrokePolyLine_rgba_color(const device char *buf, PietStrokePolyLineRef ref) { uint rgba_color = *(device const uint*)(buf + ref + 4); return rgba_color; } inline float PietStrokePolyLine_width(const device char *buf, PietStrokePolyLineRef ref) { float width = as_type(*(device const uint*)(buf + ref + 8)); return width; } inline uint PietStrokePolyLine_n_points(const device char *buf, PietStrokePolyLineRef ref) { uint n_points = *(device const uint*)(buf + ref + 12); return n_points; } inline uint PietStrokePolyLine_points_ix(const device char *buf, PietStrokePolyLineRef ref) { uint points_ix = *(device const uint*)(buf + ref + 16); return points_ix; } struct PietStrokePolyLine { uint rgba_color; float width; uint n_points; uint points_ix; }; inline PietStrokePolyLine PietStrokePolyLine_unpack(PietStrokePolyLinePacked packed_form) { PietStrokePolyLine result; result.rgba_color = packed_form.rgba_color; result.width = packed_form.width; result.n_points = packed_form.n_points; result.points_ix = packed_form.points_ix; return result; } struct PietItem { uint tag; uint body[7]; }; inline uint PietItem_tag(const device char *buf, PietItemRef ref) { uint result = *(device const uint*)(buf + ref); return result; } #define SIMPLE_GROUP_SIZE 16 #define PIET_ITEM_SIZE 32 // TODO: these are manually fixed up. Make encoders consistent #define PietItem_Circle 1 #define PietItem_Line 2 #define PietItem_Fill 3 #define PietItem_Poly 4 // Following are older-style accessors (haven't converted ptcl yet) typedef uint CmdCircleRef; typedef uint CmdLineRef; typedef uint CmdStrokeRef; typedef uint CmdFillRef; typedef uint CmdFillEdgeRef; typedef uint CmdDrawFillRef; typedef uint CmdSolidRef; typedef uint CmdRef; struct CmdCirclePacked { uint tag; ushort4 bbox; }; CmdCirclePacked CmdCircle_read(const device char *buf, CmdCircleRef ref) { return *((const device CmdCirclePacked *)(buf + ref)); } ushort4 CmdCircle_bbox(const device char *buf, CmdCircleRef ref) { return ((const device CmdCirclePacked *)(buf + ref))->bbox; } struct CmdLinePacked { uint tag; float2 start; float2 end; }; CmdLinePacked CmdLine_read(const device char *buf, CmdLineRef ref) { return *((const device CmdLinePacked *)(buf + ref)); } float2 CmdLine_start(const device char *buf, CmdLineRef ref) { return ((const device CmdLinePacked *)(buf + ref))->start; } float2 CmdLine_end(const device char *buf, CmdLineRef ref) { return ((const device CmdLinePacked *)(buf + ref))->end; } struct CmdStrokePacked { uint tag; float halfWidth; uint rgba_color; }; CmdStrokePacked CmdStroke_read(const device char *buf, CmdStrokeRef ref) { return *((const device CmdStrokePacked *)(buf + ref)); } float CmdStroke_halfWidth(const device char *buf, CmdStrokeRef ref) { return ((const device CmdStrokePacked *)(buf + ref))->halfWidth; } uint CmdStroke_rgba_color(const device char *buf, CmdStrokeRef ref) { return ((const device CmdStrokePacked *)(buf + ref))->rgba_color; } struct CmdFillPacked { uint tag; float2 start; float2 end; }; CmdFillPacked CmdFill_read(const device char *buf, CmdFillRef ref) { return *((const device CmdFillPacked *)(buf + ref)); } float2 CmdFill_start(const device char *buf, CmdFillRef ref) { return ((const device CmdFillPacked *)(buf + ref))->start; } float2 CmdFill_end(const device char *buf, CmdFillRef ref) { return ((const device CmdFillPacked *)(buf + ref))->end; } struct CmdFillEdgePacked { uint tag; int sign; float y; }; CmdFillEdgePacked CmdFillEdge_read(const device char *buf, CmdFillEdgeRef ref) { return *((const device CmdFillEdgePacked *)(buf + ref)); } int CmdFillEdge_sign(const device char *buf, CmdFillEdgeRef ref) { return ((const device CmdFillEdgePacked *)(buf + ref))->sign; } float CmdFillEdge_y(const device char *buf, CmdFillEdgeRef ref) { return ((const device CmdFillEdgePacked *)(buf + ref))->y; } struct CmdDrawFillPacked { uint tag; int backdrop; uint rgba_color; }; CmdDrawFillPacked CmdDrawFill_read(const device char *buf, CmdDrawFillRef ref) { return *((const device CmdDrawFillPacked *)(buf + ref)); } int CmdDrawFill_backdrop(const device char *buf, CmdDrawFillRef ref) { return ((const device CmdDrawFillPacked *)(buf + ref))->backdrop; } uint CmdDrawFill_rgba_color(const device char *buf, CmdDrawFillRef ref) { return ((const device CmdDrawFillPacked *)(buf + ref))->rgba_color; } struct CmdSolidPacked { uint tag; uint rgba_color; }; CmdSolidPacked CmdSolid_read(const device char *buf, CmdSolidRef ref) { return *((const device CmdSolidPacked *)(buf + ref)); } uint CmdSolid_rgba_color(const device char *buf, CmdSolidRef ref) { return ((const device CmdSolidPacked *)(buf + ref))->rgba_color; } struct Cmd { uint tag; uint body[5]; }; Cmd Cmd_read(const device char *buf, CmdRef ref) { return *((const device Cmd *)(buf + ref)); } uint Cmd_tag(const device char *buf, CmdRef ref) { return ((const device Cmd *)(buf + ref))->tag; } #define Cmd_End 1 #define Cmd_Circle 2 CmdCirclePacked CmdCircle_load(const thread Cmd &s) { CmdCirclePacked r; r.tag = s.tag; r.bbox = *((const thread ushort4 *)((const thread char *)&s + 8)); return r; } #define Cmd_Line 3 CmdLinePacked CmdLine_load(const thread Cmd &s) { CmdLinePacked r; r.tag = s.tag; r.start = *((const thread float2 *)((const thread char *)&s + 8)); r.end = *((const thread float2 *)((const thread char *)&s + 16)); return r; } #define Cmd_Fill 4 CmdFillPacked CmdFill_load(const thread Cmd &s) { CmdFillPacked r; r.tag = s.tag; r.start = *((const thread float2 *)((const thread char *)&s + 8)); r.end = *((const thread float2 *)((const thread char *)&s + 16)); return r; } #define Cmd_Stroke 5 CmdStrokePacked CmdStroke_load(const thread Cmd &s) { CmdStrokePacked r; r.tag = s.tag; r.halfWidth = *((const thread float *)((const thread char *)&s + 4)); r.rgba_color = *((const thread uint *)((const thread char *)&s + 8)); return r; } #define Cmd_FillEdge 6 CmdFillEdgePacked CmdFillEdge_load(const thread Cmd &s) { CmdFillEdgePacked r; r.tag = s.tag; r.sign = *((const thread int *)((const thread char *)&s + 4)); r.y = *((const thread float *)((const thread char *)&s + 8)); return r; } #define Cmd_DrawFill 7 CmdDrawFillPacked CmdDrawFill_load(const thread Cmd &s) { CmdDrawFillPacked r; r.tag = s.tag; r.backdrop = *((const thread int *)((const thread char *)&s + 4)); r.rgba_color = *((const thread uint *)((const thread char *)&s + 8)); return r; } #define Cmd_Solid 8 CmdSolidPacked CmdSolid_load(const thread Cmd &s) { CmdSolidPacked r; r.tag = s.tag; r.rgba_color = *((const thread uint *)((const thread char *)&s + 4)); return r; } #define Cmd_Bail 9 void CmdCircle_write(device char *buf, CmdCircleRef ref, CmdCirclePacked s) { *((device CmdCirclePacked *)(buf + ref)) = s; } void CmdLine_write(device char *buf, CmdLineRef ref, CmdLinePacked s) { *((device CmdLinePacked *)(buf + ref)) = s; } void CmdStroke_write(device char *buf, CmdStrokeRef ref, CmdStrokePacked s) { *((device CmdStrokePacked *)(buf + ref)) = s; } void CmdFill_write(device char *buf, CmdFillRef ref, CmdFillPacked s) { *((device CmdFillPacked *)(buf + ref)) = s; } void CmdFillEdge_write(device char *buf, CmdFillEdgeRef ref, CmdFillEdgePacked s) { *((device CmdFillEdgePacked *)(buf + ref)) = s; } void CmdDrawFill_write(device char *buf, CmdDrawFillRef ref, CmdDrawFillPacked s) { *((device CmdDrawFillPacked *)(buf + ref)) = s; } void CmdSolid_write(device char *buf, CmdSolidRef ref, CmdSolidPacked s) { *((device CmdSolidPacked *)(buf + ref)) = s; } void Cmd_write_tag(device char *buf, CmdRef ref, uint tag) { ((device Cmd *)(buf + ref))->tag = tag; } ================================================ FILE: TestApp/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright 2019 The xi-editor authors. NSMainStoryboardFile Main NSPrincipalClass NSApplication ================================================ FILE: TestApp/PietRender.metal ================================================ // Copyright 2019 The xi-editor authors. #include using namespace metal; #include "PietShaderTypes.h" #include "GenTypes.h" struct RenderData { float4 clipSpacePosition [[position]]; float2 textureCoordinate; float pointSize [[point_size]]; half4 solidColor; }; vertex RenderData vertexShader(uint vertexID [[ vertex_id ]], constant RenderVertex *vertexArray [[ buffer(RenderVertexInputIndexVertices) ]], texture2d loTexture [[texture(0)]]) { RenderData out; float2 clipSpacePosition = vertexArray[vertexID].position; out.clipSpacePosition.xy = clipSpacePosition; out.clipSpacePosition.z = 0.0; out.clipSpacePosition.w = 1.0; float2 xy = vertexArray[vertexID].textureCoordinate; out.textureCoordinate = xy; out.pointSize = 16; uint2 tileXY = uint2(xy.x / tileWidth, xy.y / tileHeight); out.solidColor = loTexture.read(tileXY); return out; } fragment half4 fragmentShader(RenderData in [[stage_in]], texture2d texture [[texture(0)]]) { const half4 loSample = in.solidColor; if (loSample.a == 0.0) { uint2 coords = uint2(in.clipSpacePosition.xy); const half4 sample = texture.read(coords); return sample; } else { return loSample; } } // Distance field rendering of strokes // Accumulate distance field. void stroke(thread float &df, float2 pos, float2 start, float2 end) { float2 lineVec = end - start; float2 dPos = pos - start; float t = saturate(dot(lineVec, dPos) / dot(lineVec, lineVec)); float field = length(lineVec * t - dPos); df = min(df, field); } // TODO: figure out precision so we can move more stuff to half half renderDf(float df, float halfWidth) { return saturate(halfWidth + 0.5 - df); } /* // TODO: this should be in the autogenerated output void Cmd_write_tag(device char *buf, CmdRef ref, uint tag) { ((device Cmd *)(buf + ref))->tag = tag; } */ struct TileEncoder { public: TileEncoder(device char *dst) { this->dst = dst; this->tileBegin = dst; this->solidColor = 0xffffffff; } void encodeCircle(ushort4 bbox) { CmdCirclePacked cmd; cmd.tag = Cmd_Circle; cmd.bbox = bbox; CmdCircle_write(dst, 0, cmd); solidColor = 0; dst += sizeof(Cmd); } void encodeLine(float2 start, float2 end) { CmdLinePacked cmd; cmd.tag = Cmd_Line; cmd.start = start; cmd.end = end; CmdLine_write(dst, 0, cmd); solidColor = 0; dst += sizeof(Cmd); } void encodeStroke(uint rgbaColor, float width) { CmdStrokePacked cmd; cmd.tag = Cmd_Stroke; cmd.rgba_color = rgbaColor; cmd.halfWidth = 0.5 * width; CmdStroke_write(dst, 0, cmd); solidColor = 0; dst += sizeof(Cmd); } void encodeFill(float2 start, float2 end) { CmdFillPacked cmd; cmd.tag = Cmd_Fill; cmd.start = start; cmd.end = end; CmdFill_write(dst, 0, cmd); dst += sizeof(Cmd); } void encodeFillEdge(float sign, float y) { CmdFillEdgePacked cmd; cmd.tag = Cmd_FillEdge; cmd.sign = sign; cmd.y = y; CmdFillEdge_write(dst, 0, cmd); dst += sizeof(Cmd); } void encodeDrawFill(const thread PietFillPacked &fill, int backdrop) { CmdDrawFillPacked cmd; cmd.tag = Cmd_DrawFill; cmd.backdrop = backdrop; cmd.rgba_color = fill.rgba_color; CmdDrawFill_write(dst, 0, cmd); solidColor = 0; dst += sizeof(Cmd); } void encodeSolid(uint rgba) { // A fun optimization would be to alpha-composite semi-opaque // solid blocks. // Another optimization is to skip encoding the default bg color. if ((rgba & 0xff000000) == 0xff000000) { solidColor = rgba; dst = tileBegin; } CmdSolidPacked cmd; // Note: could defer writing, not sure how much of a win that is cmd.tag = Cmd_Solid; cmd.rgba_color = rgba; CmdSolid_write(dst, 0, cmd); dst += sizeof(Cmd); } // return solid color uint end() { if (solidColor) { Cmd_write_tag(tileBegin, 0, Cmd_Bail); } else { Cmd_write_tag(dst, 0, Cmd_End); } return solidColor; } private: // Pointer to command buffer for tile. device char *dst; device char *tileBegin; uint solidColor; }; // Traverse the scene graph and produce a command list for a tile. kernel void tileKernel(device const char *scene [[buffer(0)]], device char *tiles [[buffer(1)]], texture2d outTexture [[texture(0)]], uint2 gid [[thread_position_in_grid]], uint tix [[thread_index_in_threadgroup]]) { uint tileIx = gid.y * maxTilesWidth + gid.x; ushort x0 = gid.x * tileWidth; ushort y0 = gid.y * tileHeight; device char *dst = tiles + tileIx * tileBufSize; TileEncoder encoder(dst); // TODO: correct calculation of size const ushort tgs = tilerGroupWidth * tilerGroupHeight; const ushort nBitmap = tgs / 32; threadgroup atomic_uint bitmap; threadgroup uint rd; const memory_order relaxed = memory_order::memory_order_relaxed; // Size of the region covered by one SIMD group. TODO, don't hardcode. const ushort stw = tilerGroupWidth * tileWidth; const ushort sth = tilerGroupHeight * tileHeight; ushort sx0 = x0 & ~(stw - 1); ushort sy0 = y0 & ~(sth - 1); SimpleGroupRef group_ref = 0; // TODO: write accessor functions for variable-sized array here device const SimpleGroupPacked *group = (device const SimpleGroupPacked *)scene; device const ushort4 *bboxes = (device const ushort4 *)&group->bbox; uint n = SimpleGroup_n_items(scene, group_ref); PietItemRef items_ref = SimpleGroup_items_ix(scene, group_ref); for (uint i = 0; i < n; i += tgs) { if (tix < nBitmap) { atomic_store_explicit(&bitmap, 0, relaxed); } threadgroup_barrier(mem_flags::mem_threadgroup); if (i + tix < n) { ushort4 bbox = bboxes[i + tix]; if (bbox.z >= sx0 && bbox.x < sx0 + stw && bbox.w >= sy0 && bbox.y < sy0 + sth) { uint mask = 1 << (tix & 31); atomic_fetch_or_explicit(&bitmap, mask, relaxed); } } threadgroup_barrier(mem_flags::mem_threadgroup); if (tix == 0) { rd = atomic_load_explicit(&bitmap, relaxed); atomic_store_explicit(&bitmap, 0, relaxed); } threadgroup_barrier(mem_flags::mem_threadgroup); //for (ushort bitmapIx = 0; bitmapIx <= bitmapCount; bitmapIx++) { uint v = rd; while (v) { uint ix = i + ctz(v); ushort4 bbox = bboxes[ix]; bool hit = bbox.z >= x0 && bbox.x < x0 + tileWidth && bbox.w >= y0 && bbox.y < y0 + tileHeight; PietItemRef item_ref = items_ref + ix * sizeof(PietItem); ushort itemType = PietItem_tag(scene, item_ref); switch (itemType) { case PietItem_Circle: if (hit) { encoder.encodeCircle(bbox); } break; case PietItem_Line: { // set up line equation, ax + by + c = 0 if (hit) { PietStrokeLinePacked line = PietStrokeLine_read(scene, item_ref); float a = line.end.y - line.start.y; float b = line.start.x - line.end.x; float c = -(a * line.start.x + b * line.start.y); // TODO: is this bound as tight as it can be? float hw = 0.5 * line.width + 0.5; float left = a * (x0 - hw); float right = a * (x0 + tileWidth + hw); float top = b * (y0 - hw); float bot = b * (y0 + tileHeight + hw); // If all four corners are on same side of line, cull float s00 = sign(top + left + c); float s01 = sign(top + right + c); float s10 = sign(bot + left + c); float s11 = sign(bot + right + c); if (s00 * s01 + s00 * s10 + s00 * s11 < 3.0) { encoder.encodeLine(line.start, line.end); encoder.encodeStroke(line.rgba_color, line.width); } } break; } case PietItem_Fill: { PietFillPacked fill = PietFill_read(scene, item_ref); device const float2 *pts = (device const float2 *)(scene + fill.points_ix); uint nPoints = fill.n_points; float backdrop = 0; bool anyFill = false; // use simd ballot to quick-reject segments with no contribution // Note: we just do 16 at a time for now, there's the option of doing // a 16x2 strip of tiles, with more complexity in the left-ray test. for (uint j = 0; j < nPoints; j += 16) { bool fillHit = false; uint fillIx = j + (tix & 15); if (fillIx < nPoints) { float2 start = pts[fillIx]; float2 end = pts[fillIx + 1 == nPoints ? 0 : fillIx + 1]; float2 xymin = min(start, end); float2 xymax = max(start, end); if (xymax.y >= y0 && xymin.y < y0 + tileHeight && xymin.x < sx0 + stw) { // set up line equation, ax + by + c = 0 float a = end.y - start.y; float b = start.x - end.x; float c = -(a * start.x + b * start.y); float left = a * sx0; float right = a * (sx0 + stw); float ytop = max(float(y0), xymin.y); float ybot = min(float(y0 + tileHeight), xymax.y); float top = b * ytop; float bot = b * ybot; // top left of rightmost tile in strip float sTopLeft = sign(right - a * (tileWidth) + float(y0) * b + c); float s00 = sign(top + left + c); float s01 = sign(top + right + c); float s10 = sign(bot + left + c); float s11 = sign(bot + right + c); if (sTopLeft == sign(a) && xymin.y <= y0) { // left ray intersects, need backdrop fillHit = true; } if (s00 * s01 + s00 * s10 + s00 * s11 < 3.0 && xymax.x > sx0) { // intersects strip fillHit = true; } // TODO: maybe avoid boolean - does it cost a register? if (fillHit) { atomic_fetch_or_explicit(&bitmap, 1 << tix, relaxed); } } } threadgroup_barrier(mem_flags::mem_threadgroup); if (tix == 0) { rd = atomic_load_explicit(&bitmap, relaxed); atomic_store_explicit(&bitmap, 0, relaxed); } threadgroup_barrier(mem_flags::mem_threadgroup); uint fillVote = (rd >> (tix & 16)) & 0xffff; while (fillVote) { uint fillSubIx = ctz(fillVote); fillIx = j + fillSubIx; if (hit) { float2 start = pts[fillIx]; float2 end = pts[fillIx + 1 == nPoints ? 0 : fillIx + 1]; float2 xymin = min(start, end); float2 xymax = max(start, end); // Note: no y-based cull here because it's been done in the earlier pass. // If we change that to do a strip taller than 1 tile, re-introduce here. // set up line equation, ax + by + c = 0 float a = end.y - start.y; float b = start.x - end.x; float c = -(a * start.x + b * start.y); float left = a * x0; float right = a * (x0 + tileWidth); float ytop = max(float(y0), xymin.y); float ybot = min(float(y0 + tileHeight), xymax.y); float top = b * ytop; float bot = b * ybot; // top left of tile float sTopLeft = sign(left + float(y0) * b + c); float s00 = sign(top + left + c); float s01 = sign(top + right + c); float s10 = sign(bot + left + c); float s11 = sign(bot + right + c); if (sTopLeft == sign(a) && xymin.y <= y0) { backdrop -= s00; } if (xymin.x < x0 && xymax.x > x0) { float yEdge = mix(start.y, end.y, (start.x - x0) / b); if (yEdge >= y0 && yEdge < y0 + tileHeight) { // line intersects left edge of this tile encoder.encodeFillEdge(s00, yEdge); if (b > 0.0) { encoder.encodeFill(start, float2(x0, yEdge)); } else { encoder.encodeFill(float2(x0, yEdge), end); } anyFill = true; } else if (s00 * s01 + s00 * s10 + s00 * s11 < 3.0) { encoder.encodeFill(start, end); anyFill = true; } } else if (s00 * s01 + s00 * s10 + s00 * s11 < 3.0 && xymin.x < x0 + tileWidth && xymax.x > x0) { encoder.encodeFill(start, end); anyFill = true; } } // end if (hit) fillVote &= ~(1 << fillSubIx); } } if (anyFill) { encoder.encodeDrawFill(fill, backdrop); } else if (backdrop != 0.0) { encoder.encodeSolid(fill.rgba_color); } break; } case PietItem_Poly: { PietStrokePolyLinePacked poly = PietStrokePolyLine_read(scene, item_ref); device const float2 *pts = (device const float2 *)(scene + poly.points_ix); uint nPoints = poly.n_points - 1; bool anyStroke = false; float hw = 0.5 * poly.width + 0.5; // use simd ballot to quick-reject segments with no contribution for (uint j = 0; j < nPoints; j += 32) { uint polyIx = j + tix; if (polyIx < nPoints) { float2 start = pts[polyIx]; float2 end = pts[polyIx + 1]; float2 xymin = min(start, end); float2 xymax = max(start, end); if (xymax.y > sy0 - hw && xymin.y < sy0 + sth + hw && xymax.x > sx0 - hw && xymin.x < sx0 + stw + hw) { // set up line equation, ax + by + c = 0 float a = end.y - start.y; float b = start.x - end.x; float c = -(a * start.x + b * start.y); float left = a * (sx0 - hw); float right = a * (sx0 + stw + hw); float top = b * (y0 - hw); float bot = b * (y0 + tileHeight + hw); float s00 = sign(top + left + c); float s01 = sign(top + right + c); float s10 = sign(bot + left + c); float s11 = sign(bot + right + c); if (s00 * s01 + s00 * s10 + s00 * s11 < 3.0) { // intersects strip atomic_fetch_or_explicit(&bitmap, 1 << tix, relaxed); } } } threadgroup_barrier(mem_flags::mem_threadgroup); if (tix == 0) { rd = atomic_load_explicit(&bitmap, relaxed); atomic_store_explicit(&bitmap, 0, relaxed); } threadgroup_barrier(mem_flags::mem_threadgroup); uint polyVote = rd; while (polyVote) { uint polySubIx = ctz(polyVote); polyIx = j + polySubIx; if (hit) { float2 start = pts[polyIx]; float2 end = pts[polyIx + 1]; float2 xymin = min(start, end); float2 xymax = max(start, end); if (xymax.y > y0 - hw && xymin.y < y0 + tileHeight + hw && xymax.x > x0 - hw && xymin.x < x0 + tileWidth + hw) { float a = end.y - start.y; float b = start.x - end.x; float c = -(a * start.x + b * start.y); float hw = 0.5 * poly.width + 0.5; float left = a * (x0 - hw); float right = a * (x0 + tileWidth + hw); float top = b * (y0 - hw); float bot = b * (y0 + tileHeight + hw); // If all four corners are on same side of line, cull float s00 = sign(top + left + c); float s01 = sign(top + right + c); float s10 = sign(bot + left + c); float s11 = sign(bot + right + c); if (s00 * s01 + s00 * s10 + s00 * s11 < 3.0) { encoder.encodeLine(start, end); anyStroke = true; } } } // end if (hit) polyVote &= ~(1 << polySubIx); } } if (anyStroke) { encoder.encodeStroke(poly.rgba_color, poly.width); } break; } } // end switch(itemType); v &= v - 1; } // end while (v) threadgroup_barrier(mem_flags::mem_threadgroup); //} // end for (bitmapIx) } uint solidColor = encoder.end(); outTexture.write(unpack_unorm4x8_to_half(solidColor), gid); } // Interpret the commands in the command list to produce a pixel. kernel void renderKernel(texture2d outTexture [[texture(0)]], const device char *tiles [[buffer(0)]], uint2 gid [[thread_position_in_grid]], uint2 tgid [[threadgroup_position_in_grid]]) { uint tileIx = tgid.y * maxTilesWidth + tgid.x; const device char *src = tiles + tileIx * tileBufSize; uint x = gid.x; uint y = gid.y; float2 xy = float2(x, y); // Render state (maybe factor out?) half3 rgb = half3(1.0); float df = 1e9; half signedArea = 0.0; while (1) { Cmd cmd = Cmd_read(src, 0); uint tag = cmd.tag; if (tag == Cmd_End) { break; } switch (tag) { case Cmd_Circle: { CmdCirclePacked circle = CmdCircle_load(cmd); ushort4 bbox = circle.bbox; float2 xy0 = float2(bbox.x, bbox.y); float2 xy1 = float2(bbox.z, bbox.w); float2 center = mix(xy0, xy1, 0.5); float r = length(xy - center); // I should make this shade an ellipse properly but am too lazy. // But see WebRender ellipse.glsl (linked in notes) float circleR = min(center.x - xy0.x, center.y - xy0.y); float alpha = saturate(circleR - r); rgb = mix(rgb, half3(0.0), alpha); break; } case Cmd_Line: { CmdLinePacked line = CmdLine_load(cmd); stroke(df, xy, line.start, line.end); break; } case Cmd_Stroke: { CmdStrokePacked stroke = CmdStroke_load(cmd); half alpha = renderDf(df, stroke.halfWidth); half4 fg = unpack_unorm4x8_srgb_to_half(stroke.rgba_color); rgb = mix(rgb, fg.rgb, fg.a * alpha); df = 1e9; break; } case Cmd_Fill: { CmdFillPacked fill = CmdFill_load(cmd); float2 start = fill.start - xy; float2 end = fill.end - xy; float2 window = saturate(float2(start.y, end.y)); // maybe should be an epsilon test for better numerical stability if (window.x != window.y) { float2 t = (window - start.y) / (end.y - start.y); float2 xs = mix(float2(start.x), float2(end.x), t); // This fudge factor might be inadequate when xmax is large, could // happen with small slopes. float xmin = min(min(xs.x, xs.y), 1.0) - 1e-6; float xmax = max(xs.x, xs.y); float b = min(xmax, 1.0); float c = max(b, 0.0); float d = max(xmin, 0.0); float area = (b + 0.5 * (d * d - c * c) - xmin) / (xmax - xmin); // TODO: evaluate accuracy loss from more use of half signedArea += half(area * (window.x - window.y)); } break; } case Cmd_FillEdge: { CmdFillEdgePacked fill = CmdFillEdge_load(cmd); signedArea += fill.sign * saturate(y - fill.y + 1); break; } case Cmd_DrawFill: { CmdDrawFillPacked draw = CmdDrawFill_load(cmd); half alpha = signedArea + half(draw.backdrop); alpha = min(abs(alpha), 1.0h); // nonzero winding rule // even-odd is: alpha = abs(alpha - 2.0 * round(0.5 * alpha)) // also: abs(2 * fract(0.5 * (x - 1.0)) - 1.0) half4 fg = unpack_unorm4x8_srgb_to_half(draw.rgba_color); rgb = mix(rgb, fg.rgb, fg.a * alpha); signedArea = 0.0; break; } case Cmd_Solid: { CmdSolidPacked solid = CmdSolid_load(cmd); half4 fg = unpack_unorm4x8_srgb_to_half(solid.rgba_color); rgb = mix(rgb, fg.rgb, fg.a); break; } case Cmd_Bail: return; // This case shouldn't happen, but we'll keep it for debugging. default: outTexture.write(half4(1.0, 0.0, 1.0, 1.0), gid); return; } src += sizeof(Cmd); } // Linear to sRGB conversion. Note that if we had writable sRGB textures // we could let this be done in the write call. rgb = select(1.055 * pow(rgb, 1/2.4) - 0.055, 12.92 * rgb, rgb < 0.0031308); half4 rgba = half4(rgb, 1.0); outTexture.write(rgba, gid); } ================================================ FILE: TestApp/PietRenderer.h ================================================ // Copyright 2019 The xi-editor authors. @import MetalKit; @interface PietRenderer : NSObject - (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView; @end ================================================ FILE: TestApp/PietRenderer.m ================================================ // Copyright 2019 The xi-editor authors. @import MetalKit; #import "PietRenderer.h" #import "PietShaderTypes.h" #include "piet_metal.h" @implementation PietRenderer { id _device; id _tilePipelineState; id _computePipelineState; id _renderPipelineState; id _commandQueue; id _texture; id _loTexture; id _sceneBuf; id _tileBuf; id _vertexBuf; vector_uint2 _viewportSize; } - (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView { self = [super init]; if (self) { NSError *error = NULL; _device = mtkView.device; // Note: this is consciously not sRGB, we do the conversion before writing the texture. mtkView.colorPixelFormat = MTLPixelFormatBGRA8Unorm; id defaultLibrary = [_device newDefaultLibrary]; MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; id tileFunction = [defaultLibrary newFunctionWithName:@"tileKernel"]; id kernelFunction = [defaultLibrary newFunctionWithName:@"renderKernel"]; id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"]; id fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"]; pipelineDescriptor.vertexFunction = vertexFunction; pipelineDescriptor.fragmentFunction = fragmentFunction; pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat; _tilePipelineState = [_device newComputePipelineStateWithFunction:tileFunction error:&error]; _computePipelineState = [_device newComputePipelineStateWithFunction:kernelFunction error:&error]; if (!_tilePipelineState || !_computePipelineState) { NSLog(@"Failed to create compute pipeline state, error %@", error); return nil; } _renderPipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineDescriptor error: &error]; _commandQueue = [_device newCommandQueue]; NSUInteger tileBufSizeBytes = maxTilesWidth * maxTilesHeight * tileBufSize; // Note: consider using managed here, worth experimenting with. MTLResourceOptions sceneOptions = MTLResourceStorageModeShared | MTLResourceCPUCacheModeWriteCombined; _sceneBuf = [_device newBufferWithLength:16*1024*1024 options:sceneOptions]; _tileBuf = [_device newBufferWithLength:tileBufSizeBytes options:MTLResourceStorageModePrivate]; } return self; } - (void)drawInMTKView:(nonnull MTKView *)view { id commandBuffer = [_commandQueue commandBuffer]; commandBuffer.label = @"RenderCommand"; uint nTilesX = (_viewportSize.x + tileWidth - 1) / tileWidth; uint nTilesY = (_viewportSize.y + tileHeight - 1) / tileHeight; uint nTilerGroupsX = (nTilesX + tilerGroupWidth - 1) / tilerGroupWidth; uint nTilerGroupsY = (nTilesY + tilerGroupHeight - 1) / tilerGroupHeight; // Run tile compute shader. id computeEncoder = [commandBuffer computeCommandEncoder]; [computeEncoder setComputePipelineState:_tilePipelineState]; [computeEncoder setTexture:_loTexture atIndex:0]; [computeEncoder setBuffer:_sceneBuf offset:0 atIndex:0]; [computeEncoder setBuffer:_tileBuf offset:0 atIndex:1]; MTLSize tilegroupSize = MTLSizeMake(tilerGroupWidth, tilerGroupHeight, 1); MTLSize tilegroupCount = MTLSizeMake(nTilerGroupsX, nTilerGroupsY, 1); [computeEncoder dispatchThreadgroups:tilegroupCount threadsPerThreadgroup:tilegroupSize]; [computeEncoder endEncoding]; // Run compute shader for rendering. computeEncoder = [commandBuffer computeCommandEncoder]; [computeEncoder setComputePipelineState:_computePipelineState]; [computeEncoder setTexture:_texture atIndex:0]; [computeEncoder setBuffer:_tileBuf offset:0 atIndex:0]; MTLSize threadgroupSize = MTLSizeMake(tileWidth, tileHeight, 1); MTLSize threadgroupCount = MTLSizeMake(nTilesX, nTilesY, 1); [computeEncoder dispatchThreadgroups:threadgroupCount threadsPerThreadgroup:threadgroupSize]; [computeEncoder endEncoding]; MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor; if (renderPassDescriptor != nil) { id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; [renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0}]; [renderEncoder setRenderPipelineState:_renderPipelineState]; [renderEncoder setVertexBuffer:_vertexBuf offset:0 atIndex:RenderVertexInputIndexVertices]; [renderEncoder setVertexTexture:_loTexture atIndex:0]; [renderEncoder setFragmentTexture:_texture atIndex:0]; [renderEncoder drawPrimitives:MTLPrimitiveTypePoint vertexStart:0 vertexCount:nTilesX * nTilesY]; [renderEncoder endEncoding]; [commandBuffer presentDrawable:view.currentDrawable]; } [commandBuffer commit]; } - (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size { _viewportSize.x = size.width; _viewportSize.y = size.height; // TODO: try not to allocate as wildly on smooth resize (maybe round up // the size). MTLTextureDescriptor *descriptor = [[MTLTextureDescriptor alloc] init]; descriptor.textureType = MTLTextureType2D; descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm; descriptor.width = _viewportSize.x; descriptor.height = _viewportSize.y; descriptor.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead; _texture = [_device newTextureWithDescriptor:descriptor]; uint nTilesX = (_viewportSize.x + tileWidth - 1) / tileWidth; uint nTilesY = (_viewportSize.y + tileHeight - 1) / tileHeight; descriptor.width = nTilesX; descriptor.height = nTilesY; _loTexture = [_device newTextureWithDescriptor:descriptor]; uint vertexLen = nTilesX * nTilesY * sizeof(RenderVertex); MTLResourceOptions vertexOptions = MTLResourceStorageModeShared | MTLResourceCPUCacheModeWriteCombined; _vertexBuf = [_device newBufferWithLength:vertexLen options:vertexOptions]; RenderVertex *vertices = (RenderVertex *)_vertexBuf.contents; uint ix = 0; float scaleX = 2.0 / _viewportSize.x; float scaleY = 2.0 / _viewportSize.y; for (uint y = 0; y < nTilesY; y++) { for (uint x = 0; x < nTilesX; x++) { RenderVertex rv; uint x0 = x * tileWidth + (tileWidth / 2); uint y0 = y * tileHeight + (tileHeight / 2); rv.position.x = x0 * scaleX - 1.0; rv.position.y = y0 * -scaleY + 1.0; rv.textureCoordinate.x = x0; rv.textureCoordinate.y = y0; vertices[ix++] = rv; } } [self initScene]; } /* - (void)initCardioid { float cx = 1024; float cy = 768; float r = 750; int n = 97; SceneEncoder *encoder = [[SceneEncoder alloc] initWithBuffer:_sceneBuf]; [encoder beginGroup: 2 * (n - 1)]; for (int i = 1; i < n; i++) { float th0 = 2 * M_PI * i / n; float th1 = 2 * M_PI * ((i * 2) % n) / n; vector_float2 start = simd_make_float2(cx + r * cos(th0), cy - r * sin(th0)); vector_float2 end = simd_make_float2(cx + r * cos(th1), cy - r * sin(th1)); [encoder circle:start radius: 8]; [encoder line:start to:end width:2 color:0xff800000]; } [encoder endGroup]; } - (void)fillTest { const int n = 256; SceneEncoder *encoder = [[SceneEncoder alloc] initWithBuffer:_sceneBuf]; [encoder beginGroup: n]; for (int i = 0; i < n; i++) { for (int j = 0; j < 3; j++) { uint32_t x = arc4random() % _viewportSize.x; uint32_t y = arc4random() % _viewportSize.y; [encoder addPt:simd_make_float2(x, y)]; } [encoder fill:arc4random() | 0xff000000]; } [encoder endGroup]; } - (void)initCircles { const int radius = 8; const int n = 256; SceneEncoder *encoder = [[SceneEncoder alloc] initWithBuffer:_sceneBuf]; [encoder beginGroup:n + 1]; for (int i = 0; i < n; i++) { uint32_t x = arc4random() % _viewportSize.x; uint32_t y = arc4random() % _viewportSize.y; [encoder circle:simd_make_float2(x, y) radius:radius]; } [encoder line:simd_make_float2(100, 500) to:simd_make_float2(700, 600) width:100 color:0xff800000]; [encoder endGroup]; } - (void)initScene { //[self initCircles]; //[self initCardioid]; [self fillTest]; } */ - (void)initScene { init_test_scene(_sceneBuf.contents, _sceneBuf.allocatedSize); } @end ================================================ FILE: TestApp/PietShaderTypes.h ================================================ // Copyright 2019 The xi-editor authors. typedef struct { // This is a clip space coordinate (-1 to 1). vector_float2 position; // This is now an integer coordinate for reading the texture. vector_float2 textureCoordinate; } RenderVertex; typedef enum RenderVertexInputIndex { RenderVertexInputIndexVertices = 0, } RenderVertexInputIndex; // Size in pixels of an individual tile #define tileWidth 16 #define tileHeight 16 // Size (in tiles) of a threadgroup for tiling #define tilerGroupWidth 16 #define tilerGroupHeight 2 // The number of bytes in a buffer for a single tile. // For prototyping, this is a hard maximum, but for production we'd want // a mechanism to overflow. #define tileBufSize 4096 // For simplicity, we're going to hardcode these dimensions. For production, // they need to be dynamic. #define maxTilesWidth 256 #define maxTilesHeight 256 ================================================ FILE: TestApp/SceneEncoder.h ================================================ @interface SceneEncoder: NSObject - (nonnull instancetype)initWithBuffer:(nonnull id)buffer; - (void)beginGroup:(uint)nItems; - (void)endGroup; - (void)circle:(vector_float2)center radius:(float)radius; - (void)line:(vector_float2)start to:(vector_float2)end width:(float) width color:(uint) rgba; - (void)addPt:(vector_float2)xy; - (void)fill:(uint)rgba; @end // The following are legacy definitions - encoding in ObjC will not be supported // going forward. typedef struct SimpleGroup { uint nItems; // Offset in bytes to items uint itemsIx; vector_ushort4 bbox[1]; } SimpleGroup; typedef struct PietCircle { uint itemType; } PietCircle; // A single line to be stroked, with default parameters typedef struct PietStrokeLine { uint itemType; uint flags; // reserved, partially for alignment uint rgbaColor; float width; vector_float2 start; vector_float2 end; } PietStrokeLine; typedef struct PietFill { uint itemType; uint flags; // will be used for winding number rule uint rgbaColor; uint nPoints; uint pointsIx; } PietFill; typedef struct PietStrokePolyLine { uint itemType; uint rgbaColor; float width; uint nPoints; uint pointsIx; } PietStrokePolyLine; typedef union PietItem { uint itemType; PietCircle circle; PietStrokeLine line; PietFill fill; PietStrokePolyLine poly; } PietItem; // This should be an enum but the storage needs to be of fixed size #define PIET_ITEM_CIRCLE 1 #define PIET_ITEM_LINE 2 #define PIET_ITEM_FILL 3 #define PIET_ITEM_STROKE_POLYLINE 4 ================================================ FILE: TestApp/SceneEncoder.m ================================================ // Copyright 2019 The xi-editor authors. @import MetalKit; #import "PietShaderTypes.h" #import "SceneEncoder.h" @implementation SceneEncoder { char *_buf; uint _bboxIx; uint _ix; uint _count; // Index of beginning of free space (currently allocation is just a bump). uint _freeSpace; uint _pointCount; vector_float4 _bbox; } - (nonnull instancetype)initWithBuffer:(nonnull id)buffer { _buf = buffer.contents; _ix = 0; _freeSpace = 0; _pointCount = 0; _bbox = simd_make_float4(0.0); return self; } - (uint)alloc:(uint)size { uint ix = _freeSpace; _freeSpace += size; return ix; } - (void)beginGroup:(uint)nItems { uint size = sizeof(SimpleGroup) - sizeof(vector_ushort4) + nItems * (sizeof(vector_ushort4) + sizeof(PietItem)); uint ix = [self alloc:size]; SimpleGroup *group = (SimpleGroup *)(_buf + ix); // Does zero-size array work in obj-C? _bboxIx = ix + sizeof(SimpleGroup) - sizeof(vector_ushort4); _ix = _bboxIx + sizeof(vector_ushort4) * nItems; group->nItems = nItems; group->itemsIx = _ix; _count = nItems; } - (void)endGroup { if (_count != 0) { NSLog(@"Not enough items encoded in group."); } /* for (int i = 0; i < _freeSpace / 4; i++) { NSLog(@"%04x: %08x", i * 4, ((uint *)_buf)[i]); } */ } - (void)circle:(vector_float2)center radius:(float)radius { vector_float4 bbox = simd_make_float4(center.x - radius, center.y - radius, center.x + radius, center.y + radius); PietCircle *circle = &[self allocItem:bbox]->circle; circle->itemType = PIET_ITEM_CIRCLE; } // The color argument is actually ABGR, which is the native format. // Maybe rename? - (void)line:(vector_float2)start to:(vector_float2)end width:(float) width color:(uint) rgba { float half = 0.5 * width; vector_float4 bbox = simd_make_float4(MIN(start.x, end.x) - half, MIN(start.y, end.y) - half, MAX(start.x, end.x) + half, MAX(start.y, end.y) + half); PietStrokeLine *line = &[self allocItem:bbox]->line; line->itemType = PIET_ITEM_LINE; line->rgbaColor = rgba; line->width = width; // should this be half? line->start = start; line->end = end; } // This isn't dealing with subpaths and has a crude allocation strategy. - (void)addPt:(vector_float2)xy { uint ix = [self alloc:sizeof(vector_float2)]; vector_float2 *dst = (vector_float2 *)(_buf + ix); *dst = xy; if (_pointCount == 0) { _bbox = simd_make_float4(xy, xy); } else { _bbox = simd_make_float4( MIN(_bbox.x, xy.x), MIN(_bbox.y, xy.y), MAX(_bbox.z, xy.x), MAX(_bbox.w, xy.y) ); } _pointCount++; } - (void)fill:(uint)rgba { PietFill *fill = &[self allocItem:_bbox]->fill; fill->itemType = PIET_ITEM_FILL; fill->rgbaColor = rgba; fill->nPoints = _pointCount; // This is a hack, needs to be fixed if we have real allocation. fill->pointsIx = _freeSpace - _pointCount * sizeof(vector_float2); _pointCount = 0; } - (PietItem *)allocItem:(vector_float4)bbox { if (_count == 0) { NSLog(@"encoder group count overflow"); return nil; } _count -= 1; vector_short4 *bboxPtr = (vector_short4 *)(_buf + _bboxIx); bboxPtr->x = MAX(bbox.x, 0.0); bboxPtr->y = MAX(bbox.y, 0.0); bboxPtr->z = bbox.z; bboxPtr->w = bbox.w; _bboxIx += sizeof(vector_short4); PietItem *item = (PietItem *)(_buf + _ix); _ix += sizeof(PietItem); return item; } @end ================================================ FILE: TestApp/ViewController.h ================================================ // Copyright 2019 The xi-editor authors. #import @import MetalKit; @interface ViewController : NSViewController @end ================================================ FILE: TestApp/ViewController.m ================================================ // Copyright 2019 The xi-editor authors. #import "ViewController.h" #import "PietRenderer.h" @implementation ViewController { MTKView *_view; PietRenderer *_renderer; } - (void)viewDidLoad { [super viewDidLoad]; _view = (MTKView *)self.view; _view.device = MTLCreateSystemDefaultDevice(); if(!_view.device) { NSLog(@"Metal is not supported on this device"); return; } _renderer = [[PietRenderer alloc] initWithMetalKitView:_view]; [_renderer mtkView:_view drawableSizeWillChange:_view.drawableSize]; _view.delegate = _renderer; } - (void)setRepresentedObject:(id)representedObject { [super setRepresentedObject:representedObject]; // Update the view, if already loaded. } @end ================================================ FILE: TestApp/main.m ================================================ // Copyright 2019 The xi-editor authors. #import #import "piet_metal.h" int main(int argc, const char * argv[]) { return NSApplicationMain(argc, argv); } ================================================ FILE: TestApp/piet_metal.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only ================================================ FILE: include/piet_metal.h ================================================ #include void init_test_scene(uint8_t *buf, ssize_t buf_size); ================================================ FILE: piet-gpu-derive/Cargo.toml ================================================ [package] name = "piet-gpu-derive" version = "0.0.0" authors = ["Raph Levien "] description = "Proc macro derives for piet-gpu." license = "MIT/Apache-2.0" edition = "2018" keywords = ["graphics", "2d"] categories = ["rendering::graphics-api"] [lib] proc-macro = true [dependencies] syn = {version = "1.0.5", features = ["extra-traits", "full"]} quote = "1.0.2" proc-macro2 = "1.0.4" ================================================ FILE: piet-gpu-derive/src/lib.rs ================================================ //! TODO: explain in detail how this works. //! //! A few notes that will be helpful. Structs are encoded differently depending //! on whether they appear as a variant in an enum; if so, the tag is included. //! This allows the alignment of the struct to take the tag into account. extern crate proc_macro; #[macro_use] extern crate quote; use std::collections::HashSet; use std::fmt::Write; use std::ops::Deref; use proc_macro::TokenStream; use syn::parse_macro_input; use syn::{ Expr, ExprLit, Fields, FieldsNamed, FieldsUnnamed, GenericArgument, ItemEnum, ItemStruct, Lit, PathArguments, TypeArray, TypePath, }; /// The target shader language. We can't make this a public type because of Rust rules. #[derive(Copy, Clone, PartialEq)] enum TargetLang { Hlsl, Msl, } /// A scalar that can be represented in a packed data structure. #[derive(Clone, Copy, PartialEq)] enum GpuScalar { I8, I16, I32, F32, U8, U16, U32, // TODO: Add F16 } /// An algebraic datatype. #[derive(Clone)] enum GpuType { Scalar(GpuScalar), Vector(GpuScalar, usize), /// Used mostly for the body of enum variants. InlineStruct(String), Ref(Box), } struct GpuEnum { name: String, variants: Vec<(String, Vec)>, } enum GpuTypeDef { Struct(String, Vec<(String, GpuType)>), Enum(GpuEnum), } struct GpuModule { #[allow(unused)] name: String, /// Set of item names that are used as enum variants. enum_variants: HashSet, defs: Vec, } impl TargetLang { /// The typed function argument for "buf" fn buf_arg(self) -> &'static str { match self { TargetLang::Hlsl => "ByteAddressBuffer buf", TargetLang::Msl => "const device char *buf", } } /// An expression for loading a number of uints. fn load_expr(self, offset: usize, size: usize) -> String { let tail = if offset == 0 { "".into() } else { format!(" + {}", offset) }; let size_str = vector_size_str(size); match self { TargetLang::Hlsl => format!("buf.Load{}(ref{})", size_str, tail), TargetLang::Msl => { let packed = if size == 1 { "" } else { "packed_" }; format!( "*(device const {}uint{}*)(buf + ref{})", packed, size_str, tail ) } } } } impl GpuScalar { /// The unpacked type of the scalar value. fn unpacked_type(self, target: TargetLang) -> GpuScalar { match target { TargetLang::Hlsl => match self { GpuScalar::I8 | GpuScalar::I16 => GpuScalar::I32, GpuScalar::U8 | GpuScalar::U16 => GpuScalar::U32, _ => self, }, _ => self, } } fn typename(self, target: TargetLang) -> &'static str { if target == TargetLang::Hlsl && self.size() < 4 { panic!( "Internal logic error: trying to determine HLSL typename for {} byte value", self.size() ); } match self { GpuScalar::F32 => "float", GpuScalar::I8 => "char", GpuScalar::I16 => "short", GpuScalar::I32 => "int", GpuScalar::U8 => "uchar", GpuScalar::U16 => "ushort", GpuScalar::U32 => "uint", } } fn size(self) -> usize { match self { GpuScalar::F32 | GpuScalar::I32 | GpuScalar::U32 => 4, GpuScalar::I8 | GpuScalar::U8 => 1, GpuScalar::I16 | GpuScalar::U16 => 2, } } /// Convert an expression with type "uint" into the given scalar. fn cvt(self, inner: &str, target: TargetLang) -> String { self.cvt_vec(inner, 1, target) } /// Convert a uint vector into the given vector fn cvt_vec(self, inner: &str, size: usize, target: TargetLang) -> String { let size = vector_size_str(size); match (target, self) { (TargetLang::Hlsl, GpuScalar::F32) => format!("asfloat({})", inner), (TargetLang::Hlsl, GpuScalar::I32) => format!("asint({})", inner), (TargetLang::Msl, GpuScalar::F32) => format!("as_type({})", size, inner), (TargetLang::Msl, GpuScalar::I32) => format!("as_type({})", size, inner), // TODO: need to be smarter about signed int conversion _ => inner.into(), } } fn from_syn(ty: &syn::Type) -> Option { ty_as_single_ident(ty).and_then(|ident| match ident.as_str() { "f32" => Some(GpuScalar::F32), "i8" => Some(GpuScalar::I8), "i16" => Some(GpuScalar::I16), "i32" => Some(GpuScalar::I32), "u8" => Some(GpuScalar::U8), "u16" => Some(GpuScalar::U16), "u32" => Some(GpuScalar::U32), _ => None, }) } } impl std::fmt::Display for GpuScalar { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { GpuScalar::F32 => write!(f, "F32"), GpuScalar::I8 => write!(f, "I8"), GpuScalar::I16 => write!(f, "I16"), GpuScalar::I32 => write!(f, "I32"), GpuScalar::U8 => write!(f, "U8"), GpuScalar::U16 => write!(f, "U16"), GpuScalar::U32 => write!(f, "U32"), } } } /// If `c = 0`, return `"var_name`, else `"var_name + c"` fn simplified_add(var_name: &str, c: usize) -> String { if c == 0 { String::from(var_name) } else { format!("{} + {}", var_name, c) } } /// Suffix to add to scalar type to make it into a vector. /// /// For size of 1, returns empty string, though "usize1" is usually valid. /// This is so we have one name for the same type, and also so the suffix /// can be used for `ByteAddressBuf.Load` method names. fn vector_size_str(size: usize) -> &'static str { match size { 1 => "", 2 => "2", 3 => "3", 4 => "4", _ => panic!("illegal vector size {}", size) } } /// Return number of `uints` required to store `num_bytes` bytes. fn size_in_uints(num_bytes: usize) -> usize { // a `uint` has a size of 4 bytes, (size_in_bytes + 4 - 1) / 4 (num_bytes + 3) / 4 } // TODO: this only generates unsigned extractors, we will need signed as well. fn generate_value_extractor(size_in_bits: u32) -> String { if size_in_bits > 31 { panic!("nonsensical to generate an extractor for a value with bit size greater than 31"); } let mut extractor: String = String::new(); let mask_width: usize = 2_usize.pow(size_in_bits) - 1; write!( extractor, "inline uint extract_{}bit_value(uint bit_shift, uint package) {{\n", size_in_bits ) .unwrap(); write!(extractor, " uint mask = {};\n", mask_width).unwrap(); write!( extractor, "{}", " uint result = (package >> bit_shift) & mask;\n\n return result;\n}\n\n" ) .unwrap(); extractor } /// A `PackedField` stores `StoredField`s #[derive(Clone)] struct StoredField { name: String, ty: GpuType, /// The offset of the field within the packed field, in bits. offset: usize, } /// A `PackedStruct` has `PackedField`s #[derive(Clone)] struct PackedField { name: String, /// The type of the package, as stored packed. ty: Option, stored_fields: Vec, size: usize, } /// Possible results of the `pack` method on a `PackedField`. #[derive(PartialEq)] enum PackResult { SuccessAndOpen, SuccessAndClosed, FailAndClosed, } #[derive(Clone)] struct PackedStruct { name: String, packed_fields: Vec, is_enum_variant: bool, } struct SpecifiedStruct { name: String, fields: Vec<(String, GpuType)>, packed_form: PackedStruct, } impl StoredField { fn generate_unpacker( &self, packed_struct_name: &str, packed_field_name: &str, target: TargetLang, ) -> String { let mut unpacker = String::new(); // A hack to get the base struct name let stripped_name = &packed_struct_name[0..packed_struct_name.len() - 6]; if self.ty.is_small() { match self.ty { GpuType::Scalar(scalar) => { let size_in_bits = 8 * scalar.size(); let hlsl_typename: String = match scalar { GpuScalar::F32 | GpuScalar::I32 | GpuScalar::U32 => { panic!("unexpected unpacking of 32 bit value!") } _ => String::from(scalar.unpacked_type(target).typename(target)), }; write!( unpacker, "inline uint {}_unpack_{}(uint {}) {{\n {} result;\n\n", stripped_name, self.name, packed_field_name, hlsl_typename, ) .unwrap(); write!( unpacker, " result = extract_{}bit_value({}, {});\n", size_in_bits, self.offset, packed_field_name ) .unwrap(); } GpuType::Vector(scalar, unpacked_size) => { let scalar_size_in_bits = 8 * scalar.size(); let unpacked_typename = self.ty.unpacked_typename(target); let size_in_uints = size_in_uints(&scalar.size() * unpacked_size); write!( unpacker, "inline {} {}_unpack_{}(uint{} {}) {{\n {} result;\n\n", unpacked_typename, stripped_name, self.name, size_in_uints, packed_field_name, unpacked_typename, ) .unwrap(); for i in 0..unpacked_size { let subscript = if size_in_uints == 1 { "".into() } else { format!("[{}]", (i * scalar_size_in_bits) / 32) }; let extracted = scalar.cvt( &format!( "extract_{}bit_value({}, {}{})", scalar_size_in_bits, self.offset + (i * scalar_size_in_bits) % 32, packed_field_name, subscript ), target, ); write!(unpacker, " result[{}] = {};\n", i, extracted).unwrap(); } } _ => panic!( "only expected small types, got: {}", self.ty.unpacked_typename(target) ), } write!(unpacker, "{}", " return result;\n").unwrap(); write!(unpacker, "{}", "}\n\n").unwrap(); } unpacker } } impl PackedField { fn new() -> PackedField { PackedField { name: String::new(), ty: None, size: 0, stored_fields: vec![], } } fn pack( &mut self, module: &GpuModule, field_type: &GpuType, field_name: &str, ) -> Result { if !self.is_closed() { let field_size = field_type.size(module); if field_size + self.size > 4 { if self.is_empty() { self.stored_fields.push(StoredField { name: field_name.into(), ty: field_type.clone(), offset: 0, }); self.close(module).unwrap(); Ok(PackResult::SuccessAndClosed) } else { self.close(module).unwrap(); Ok(PackResult::FailAndClosed) } } else { self.stored_fields.push(StoredField { name: String::from(field_name), ty: field_type.clone(), offset: self.size * 8, }); self.size += field_size; Ok(PackResult::SuccessAndOpen) } } else { Err("cannot extend closed package".into()) } } fn is_empty(&self) -> bool { self.stored_fields.is_empty() } fn is_closed(&self) -> bool { self.ty.is_some() } /// True when the packed and unpacked types differ. fn is_packed(&self, struct_result: bool) -> bool { if self.stored_fields.len() != 1 { return true; } match self.stored_fields[0].ty { GpuType::Scalar(scalar) => scalar.size() < 4, GpuType::Vector(scalar, _) => scalar.size() < 4, GpuType::InlineStruct(_) => struct_result, _ => false, } } fn close(&mut self, module: &GpuModule) -> Result<(), String> { if !self.is_closed() { if self.is_empty() { Err("cannot close empty package".into()) } else { let stored_field_names = self .stored_fields .iter() .map(|pf| pf.name.clone()) .collect::>(); self.name = stored_field_names.join("_"); if self.is_packed(false) { let summed_size = self.stored_fields.iter().map(|pf| pf.ty.size(module)).sum(); let size_in_uints = size_in_uints(summed_size); if size_in_uints == 1 { self.ty = Some(GpuType::Scalar(GpuScalar::U32)); } else { self.ty = Some(GpuType::Vector(GpuScalar::U32, size_in_uints)); } } else { self.ty = Some(self.stored_fields[0].ty.clone()); } Ok(()) } } else { Err("cannot close closed package".into()) } } fn generate_reader( &self, current_offset: usize, target: TargetLang, ) -> Result { if let Some(ty) = &self.ty { let type_name = ty.unpacked_typename(target); let packed_field_name = &self.name; match ty { GpuType::Scalar(scalar) => { let load_expr = target.load_expr(current_offset, 1); let cvt_exp = scalar.cvt(&load_expr, target); Ok(format!( " {} {} = {};\n", type_name, packed_field_name, cvt_exp, )) } GpuType::Vector(scalar, size) => { let size_in_uints = size_in_uints(scalar.size() * size); let load_expr = target.load_expr(current_offset, size_in_uints); let cvt_exp = scalar.cvt_vec(&load_expr, *size, target); Ok(format!( " {}{} {} = {};\n", scalar.typename(target), size, packed_field_name, cvt_exp, )) } GpuType::InlineStruct(isn) => Ok(format!( " {}Packed {} = {}_read(buf, {});\n", isn, packed_field_name, isn, simplified_add("ref", current_offset) )), GpuType::Ref(inner) => { if let GpuType::InlineStruct(isn) = inner.deref() { Ok(format!( " {}Ref {} = {};\n", isn, packed_field_name, target.load_expr(current_offset, 1), )) } else { Ok(format!( " uint {} = {};\n", packed_field_name, target.load_expr(current_offset, 1), )) } } } } else { Err("cannot generate field reader from an open packed field".into()) } } fn generate_accessor( &self, packed_struct_name: &str, ref_type: &str, reader: &str, target: TargetLang, ) -> Result { if let Some(ty) = &self.ty { let mut field_accessor = String::new(); match ty { GpuType::InlineStruct(name) => { write!( field_accessor, "inline {}Packed {}_{}({}, {} ref) {{\n", name, packed_struct_name, self.name, target.buf_arg(), ref_type, ) .unwrap(); } _ => { write!( field_accessor, "inline {} {}_{}({}, {} ref) {{\n", ty.unpacked_typename(target), packed_struct_name, self.name, target.buf_arg(), ref_type, ) .unwrap(); } } write!(field_accessor, "{}", reader).unwrap(); write!(field_accessor, " return {};\n}}\n\n", self.name).unwrap(); Ok(field_accessor) } else { Err("cannot generate field accessor from open packed field".into()) } } fn generate_unpackers(&self, packed_struct_name: &str, target: TargetLang) -> String { let mut unpackers = String::new(); for sf in &self.stored_fields { write!( unpackers, "{}", sf.generate_unpacker(packed_struct_name, &self.name, target) ) .unwrap(); } unpackers } fn size(&self, module: &GpuModule) -> Result { if let Some(ty) = &self.ty { Ok(ty.size(module)) } else { Err("cannot calculate size of open packed field".into()) } } } impl PackedStruct { fn new(module: &GpuModule, name: &str, fields: &Vec<(String, GpuType)>) -> PackedStruct { let mut packed_fields: Vec = Vec::new(); let mut current_packed_field = PackedField::new(); for (field_name, ty) in fields { match current_packed_field.pack(module, &ty, &field_name).unwrap() { PackResult::SuccessAndClosed => { packed_fields.push(current_packed_field); current_packed_field = PackedField::new(); } PackResult::FailAndClosed => { packed_fields.push(current_packed_field); current_packed_field = PackedField::new(); let res = current_packed_field.pack(module, &ty, &field_name).unwrap(); if res == PackResult::SuccessAndClosed { packed_fields.push(current_packed_field); current_packed_field = PackedField::new(); } } _ => {} } } if !current_packed_field.is_closed() { if !current_packed_field.is_empty() { current_packed_field.close(module).unwrap(); packed_fields.push(current_packed_field); } } PackedStruct { name: format!("{}Packed", name), packed_fields, is_enum_variant: module.enum_variants.contains(name), } } fn generate_functions(&self, module: &GpuModule, target: TargetLang) -> String { let mut r = String::new(); let mut field_accessors: Vec = Vec::new(); let mut unpackers: Vec = Vec::new(); // This is something of a hack to strip the "Packed" off the struct name let stripped_name = &self.name[0..self.name.len() - 6]; let ref_type = format!("{}Ref", stripped_name); write!( r, "inline {} {}_read({}, {} ref) {{\n", self.name, stripped_name, target.buf_arg(), ref_type, ) .unwrap(); write!(r, " {} result;\n\n", self.name).unwrap(); let mut current_offset: usize = 0; if self.is_enum_variant { // account for tag current_offset = 4; } for packed_field in &self.packed_fields { let reader: String = packed_field .generate_reader(current_offset, target) .unwrap(); let field_accessor: String = packed_field .generate_accessor(stripped_name, &ref_type, &reader, target) .unwrap(); field_accessors.push(field_accessor); if packed_field.is_packed(false) { unpackers.push(packed_field.generate_unpackers(&self.name, target)); } write!(r, "{}", reader).unwrap(); write!( r, " result.{} = {};\n\n", packed_field.name, packed_field.name ) .unwrap(); current_offset += packed_field.size(module).unwrap(); } write!(r, " return result;\n}}\n\n",).unwrap(); for field_accessor in field_accessors { write!(r, "{}", field_accessor).unwrap(); } for unpacker in unpackers { write!(r, "{}", unpacker).unwrap(); } r } fn generate_structure_def(&self, target: TargetLang) -> String { let mut r = String::new(); // The packed struct definition (is missing variable sized arrays) write!(r, "struct {} {{\n", self.name).unwrap(); if self.is_enum_variant { write!(r, " uint tag;\n").unwrap(); } for packed_field in self.packed_fields.iter() { match packed_field.ty.as_ref().unwrap() { GpuType::InlineStruct(name) => { // a packed struct will only store the packed version of any structs write!(r, " {}Packed {};\n", name, packed_field.name) } _ => write!( r, " {} {};\n", packed_field .ty .as_ref() .expect(&format!("packed field {} has no type", packed_field.name)) .unpacked_typename(target), packed_field.name ), } .unwrap() } write!(r, "{}", "};\n\n").unwrap(); r } fn to_shader(&self, module: &GpuModule, target: TargetLang) -> String { let mut r = String::new(); write!(r, "{}", self.generate_structure_def(target)).unwrap(); write!(r, "{}", self.generate_functions(module, target)).unwrap(); r } } impl SpecifiedStruct { fn new(module: &GpuModule, name: &str, fields: Vec<(String, GpuType)>) -> SpecifiedStruct { let packed_form = PackedStruct::new(module, name, &fields); SpecifiedStruct { name: name.to_string(), fields, packed_form, } } fn generate_structure_def(&self, target: TargetLang) -> String { let mut r = String::new(); // The unpacked struct definition (is missing variable sized arrays) write!(r, "struct {} {{\n", self.name).unwrap(); for (field_name, field_type) in self.fields.iter() { write!( r, " {} {};\n", field_type.unpacked_typename(target), field_name ) .unwrap() } write!(r, "{}", "};\n\n").unwrap(); r } fn generate_unpacker(&self) -> String { let mut r = String::new(); write!( r, "inline {} {}_unpack({} packed_form) {{\n", self.name, self.name, self.packed_form.name, ) .unwrap(); write!(r, " {} result;\n\n", self.name).unwrap(); for (field_name, field_type) in self.fields.iter() { let packed_field = self .packed_form .packed_fields .iter() .find(|&pf| { pf.stored_fields .iter() .find(|&sf| sf.name == field_name.as_str()) .is_some() }) .expect(&format!( "no packed field stores {} in {}Packed", field_name, self.name )); if packed_field.is_packed(true) { match field_type { GpuType::InlineStruct(name) => { write!( r, " result.{} = {}_unpack(packed_form.{});\n", field_name, name, packed_field.name ) .unwrap(); } _ => { write!( r, " result.{} = {}_unpack_{}(packed_form.{});\n", field_name, self.name, field_name, packed_field.name ) .unwrap(); } } } else { write!( r, " result.{} = packed_form.{};\n", field_name, packed_field.name ) .unwrap(); } } write!(r, "{}", "\n return result;\n}\n\n").unwrap(); r } fn to_shader(&self, target: TargetLang) -> String { let mut r = String::new(); write!(r, "{}", self.generate_structure_def(target)).unwrap(); write!(r, "{}", self.generate_unpacker()).unwrap(); r } } impl GpuType { // The type name for the *unpacked* version of the type. fn unpacked_typename(&self, target: TargetLang) -> String { match self { GpuType::Scalar(scalar) => scalar.unpacked_type(target).typename(target).into(), GpuType::Vector(scalar, size) => match scalar { GpuScalar::F32 | GpuScalar::I32 | GpuScalar::U32 => { format!("{}{}", scalar.unpacked_type(target).typename(target), size) } _ => { if *size == 1 { "uint".into() } else { format!("uint{}", size) } } }, GpuType::InlineStruct(name) => name.to_string(), // TODO: probably want to have more friendly names for simple struct refs. GpuType::Ref(inner) => { if let GpuType::InlineStruct(name) = inner.deref() { format!("{}Ref", name) } else { "uint".into() } } } } fn size(&self, module: &GpuModule) -> usize { match self { GpuType::Scalar(scalar) => scalar.size(), GpuType::Vector(scalar, size) => scalar.size() * size, GpuType::InlineStruct(name) => module.resolve_by_name(&name).unwrap().size(module), GpuType::Ref(_name) => 4, } } fn alignment(&self, module: &GpuModule) -> usize { // TODO: there are alignment problems with vectors of 3 match self { GpuType::Scalar(scalar) => scalar.size(), GpuType::Vector(scalar, size) => scalar.size() * size, GpuType::InlineStruct(name) => module.resolve_by_name(&name).unwrap().alignment(module), GpuType::Ref(_name) => 4, } } /// Report whether type is a scalar or simple vector fn is_small(&self) -> bool { match self { GpuType::Scalar(_) => true, GpuType::Vector(_, _) => true, GpuType::InlineStruct(_) => false, GpuType::Ref(_) => true, } } fn from_syn(ty: &syn::Type) -> Result { //println!("gputype {:#?}", ty); if let Some(scalar) = GpuScalar::from_syn(ty) { return Ok(GpuType::Scalar(scalar)); } if let Some(name) = ty_as_single_ident(ty) { // Note: we're not doing any validation here. return Ok(GpuType::InlineStruct(name)); } match ty { syn::Type::Path(TypePath { path: syn::Path { segments, .. }, .. }) => { if segments.len() == 1 { let seg = &segments[0]; if seg.ident == "Ref" { if let PathArguments::AngleBracketed(args) = &seg.arguments { if args.args.len() == 1 { if let GenericArgument::Type(inner) = &args.args[0] { let inner_ty = GpuType::from_syn(inner)?; return Ok(GpuType::Ref(Box::new(inner_ty))); } } } } } Err("unknown path case".into()) } syn::Type::Array(TypeArray { elem, len, .. }) => { if let Some(elem) = GpuScalar::from_syn(&elem) { if let Some(len) = expr_int_lit(len) { // maybe sanity-check length here Ok(GpuType::Vector(elem, len)) } else { Err("can't deal with variable length scalar arrays".into()) } } else { Err("can't deal with non-scalar arrays".into()) } } _ => Err("unknown type".into()), } } } impl GpuTypeDef { fn from_syn(item: &syn::Item) -> Result { match item { syn::Item::Struct(ItemStruct { ident, fields: Fields::Named(FieldsNamed { named, .. }), .. }) => { let mut fields = Vec::new(); for field in named { let field_ty = GpuType::from_syn(&field.ty)?; let field_name = field.ident.as_ref().ok_or("need name".to_string())?; fields.push((field_name.to_string(), field_ty)); } Ok(GpuTypeDef::Struct(ident.to_string(), fields)) } syn::Item::Enum(ItemEnum { ident, variants, .. }) => { let mut v = Vec::new(); for variant in variants { let vname = variant.ident.to_string(); let mut fields = Vec::new(); if let Fields::Unnamed(FieldsUnnamed { unnamed, .. }) = &variant.fields { for field in unnamed { fields.push(GpuType::from_syn(&field.ty)?); } } v.push((vname, fields)); } let en = GpuEnum { name: ident.to_string(), variants: v, }; Ok(GpuTypeDef::Enum(en)) } _ => { eprintln!("{:#?}", item); Err("unknown item".into()) } } } fn name(&self) -> &str { match self { GpuTypeDef::Struct(name, _) => &name, GpuTypeDef::Enum(en) => &en.name, } } fn collect_refs(&self, enum_variants: &mut HashSet) { if let GpuTypeDef::Enum(en) = self { for variant in &en.variants { if let Some(GpuType::InlineStruct(name)) = variant.1.first() { enum_variants.insert(name.clone()); } } } } /// Size of the body of the definition. fn size(&self, module: &GpuModule) -> usize { match self { GpuTypeDef::Struct(name, fields) => { let mut offset = 0; if module.enum_variants.contains(name) { offset += 4; } for (_name, field) in fields { offset += align_padding(offset, field.alignment(module)); offset += field.size(module); } offset } GpuTypeDef::Enum(en) => { let mut max_offset = 4; for (_name, fields) in &en.variants { let mut offset = 4; for field in fields { if let GpuType::InlineStruct(_) = field { if offset == 4 { offset = 0; } } // Alignment needs work :/ //offset += align_padding(offset, field.alignment(module)); offset += field.size(module); } max_offset = max_offset.max(offset); } max_offset } } } /// Alignment of the body of the definition. fn alignment(&self, module: &GpuModule) -> usize { match self { GpuTypeDef::Struct(name, fields) => { let mut alignment = 1; if module.enum_variants.contains(name) { alignment = 4; } for (_name, field) in fields { alignment = alignment.max(field.alignment(module)); } alignment } GpuTypeDef::Enum(_en) => unimplemented!(), } } // TODO: implement this in new scheme /* fn to_metal_load_enum(&self, enum_name: &str, module: &GpuModule, r: &mut String) { match self { GpuTypeDef::Struct(name, fields) => { write!( r, "{}Packed {}_load(const thread {} &s) {{\n", name, name, enum_name ) .unwrap(); write!(r, " {}Packed r;\n", name).unwrap(); write!(r, " r.tag = s.tag;\n").unwrap(); let mut offset = 4; for (fieldname, ty) in fields { offset += align_padding(offset, ty.alignment(module)); let mty = ty.metal_typename(); // maybe better to load from `body` array rather than pointer foo write!( r, " r.{} = *((const thread {} *)((const thread char *)&s + {}));\n", fieldname, mty, offset ) .unwrap(); offset += ty.size(module); } write!(r, " return r;\n").unwrap(); write!(r, "}}\n").unwrap(); } _ => panic!("internal inconsistency"), } } */ // TODO: implement writers /* fn to_metal_wr(&self, _module: &GpuModule) -> String { let mut r = String::new(); match self { GpuTypeDef::Struct(name, _fields) => { // Write of packed structure write!( r, "void {}_write(device char *buf, {}Ref ref, {}Packed s) {{\n", name, name, name ) .unwrap(); write!(r, " *((device {}Packed *)(buf + ref)) = s;\n", name).unwrap(); write!(r, "}}\n").unwrap(); } // We don't write individual enum structs, we only write their variants. GpuTypeDef::Enum(en) => { if en.variants.iter().any(|(_name, fields)| fields.is_empty()) { write!( r, "void {}_write_tag(device char *buf, CmdRef ref, uint tag) {{\n", en.name ) .unwrap(); write!(r, " ((device {} *)(buf + ref))->tag = tag;\n", en.name).unwrap(); write!(r, "}}\n").unwrap(); } } } r } */ fn to_shader(&self, module: &GpuModule, target: TargetLang) -> String { let mut r = String::new(); match self { GpuTypeDef::Struct(name, fields) => { let structure = SpecifiedStruct::new(module, name, fields.clone()); write!(r, "{}", structure.packed_form.to_shader(module, target)).unwrap(); write!(r, "{}", structure.to_shader(target)).unwrap(); } GpuTypeDef::Enum(en) => { let rn = format!("{}Ref", en.name); write!(r, "struct {} {{\n", en.name).unwrap(); write!(r, " uint tag;\n").unwrap(); let size = self.size(module); // TODO: this sometimes predicts incorrect number of u32s needed to store body (differences with metal alignment) let body_size = ((size + 3) >> 2) - 1; write!(r, " uint body[{}];\n", body_size).unwrap(); write!(r, "}};\n").unwrap(); write!( r, "inline uint {}_tag({}, {} ref) {{\n", en.name, target.buf_arg(), rn ) .unwrap(); write!( r, " uint result = {};\n return result;\n", target.load_expr(0, 1) ) .unwrap(); write!(r, "}}\n\n").unwrap(); if target == TargetLang::Hlsl { let quotient_in_u32x4 = size / (4 * GpuScalar::U32.size()); let remainder_in_u32s = (size / 4) % 4; write!(r, "inline void {}_copy(ByteAddressBuffer src, uint src_ref, RWByteAddressBuffer dst, uint dst_ref) {{\n", en.name).unwrap(); for i in 0..quotient_in_u32x4 { write!( r, " uint4 group{} = src.Load4({});\n", i, simplified_add("src_ref", i * 4 * 4) ) .unwrap(); write!( r, " dst.Store4({}, group{});\n", simplified_add("dst_ref", i * 4 * 4), i, ) .unwrap(); } if remainder_in_u32s > 0 { let tail = vector_size_str(remainder_in_u32s); write!( r, "\n uint{} group{} = src.Load{}({});\n", tail, quotient_in_u32x4, tail, simplified_add("src_ref", quotient_in_u32x4 * 4 * 4) ) .unwrap(); write!( r, " dst.Store{}({}, group{});\n", tail, simplified_add("dst_ref", quotient_in_u32x4 * 4 * 4), quotient_in_u32x4 ) .unwrap(); } write!(r, "{}", "}\n\n").unwrap(); } } } r } } impl GpuModule { fn from_syn(module: &syn::ItemMod) -> Result { let name = module.ident.to_string(); let mut defs = Vec::new(); let mut enum_variants = HashSet::new(); if let Some((_brace, items)) = &module.content { for item in items { let def = GpuTypeDef::from_syn(item)?; def.collect_refs(&mut enum_variants); defs.push(def); } } Ok(GpuModule { name, enum_variants, defs, }) } fn resolve_by_name(&self, name: &str) -> Result<&GpuTypeDef, String> { for def in &self.defs { if def.name() == name { return Ok(&def); } } Err(format!("could not find {} in module", name)) } fn to_shader(&self, target: TargetLang) -> String { let mut r = String::new(); write!(&mut r, "{}", generate_value_extractor(8)).unwrap(); write!(&mut r, "{}", generate_value_extractor(16)).unwrap(); for def in &self.defs { match def { GpuTypeDef::Struct(name, _) => { write!(&mut r, "typedef uint {}Ref;\n", name).unwrap(); } GpuTypeDef::Enum(_) => { write!(&mut r, "typedef uint {}Ref;\n", def.name()).unwrap(); } } } write!(&mut r, "\n").unwrap(); for def in &self.defs { r.push_str(&def.to_shader(self, target)); } for def in &self.defs { let name = def.name(); if !(self.enum_variants.contains(name)) { write!( r, "#define {}_SIZE {}\n", to_snake_case(name).to_uppercase(), def.size(self) ) .unwrap(); } if let GpuTypeDef::Enum(en) = def { let mut tag: usize = 0; for (name, _fields) in &en.variants { write!(r, "#define {}_{} {}\n", en.name, name, tag).unwrap(); tag += 1; } } } r } } fn ty_as_single_ident(ty: &syn::Type) -> Option { if let syn::Type::Path(TypePath { path: syn::Path { segments, .. }, .. }) = ty { if segments.len() == 1 { let seg = &segments[0]; if seg.arguments == PathArguments::None { return Some(seg.ident.to_string()); } } } None } fn expr_int_lit(e: &Expr) -> Option { if let Expr::Lit(ExprLit { lit: Lit::Int(lit_int), .. }) = e { lit_int.base10_parse().ok() } else { None } } fn align_padding(offset: usize, alignment: usize) -> usize { offset.wrapping_neg() & (alignment - 1) } /* TODO: make derive macros #[proc_macro_derive(PietMetal)] pub fn derive_piet_metal(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as syn::DeriveInput); derive_proc_metal_impl(input) .unwrap_or_else(|err| err.to_compile_error()) .into() } fn derive_proc_metal_impl(input: syn::DeriveInput) -> Result { println!("input: {:#?}", input); match &input.data { Data::Struct { .. } => { println!("it's a struct!"); } _ => (), } let s = "this is a string"; let expanded = quote! { fn foo() { println!("this was generated by proc macro: {}", #s); } }; Ok(expanded) } */ #[proc_macro] pub fn piet_gpu(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as syn::ItemMod); //println!("input: {:#?}", input); let module = GpuModule::from_syn(&input).unwrap(); let gen_gpu_fn = format_ident!("gen_gpu_{}", input.ident); let hlsl_result = module.to_shader(TargetLang::Hlsl); let msl_result = module.to_shader(TargetLang::Msl); let expanded = quote! { fn #gen_gpu_fn(lang: &str) -> String { match lang { "HLSL" => #hlsl_result.into(), "MSL" => #msl_result.into(), _ => panic!("unkonwn shader lang {}", lang), } } }; expanded.into() } fn to_snake_case(mut str: &str) -> String { let mut words = vec![]; // Preserve leading underscores str = str.trim_start_matches(|c: char| { if c == '_' { words.push(String::new()); true } else { false } }); for s in str.split('_') { let mut last_upper = false; let mut buf = String::new(); if s.is_empty() { continue; } for ch in s.chars() { if !buf.is_empty() && buf != "'" && ch.is_uppercase() && !last_upper { words.push(buf); buf = String::new(); } last_upper = ch.is_uppercase(); buf.extend(ch.to_lowercase()); } words.push(buf); } words.join("_") } ================================================ FILE: piet-metal.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 50; objects = { /* Begin PBXBuildFile section */ AE20EE122277739F00207011 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AE20EE112277739F00207011 /* AppDelegate.m */; }; AE20EE152277739F00207011 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE20EE142277739F00207011 /* ViewController.m */; }; AE20EE17227773A000207011 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE20EE16227773A000207011 /* Assets.xcassets */; }; AE20EE1A227773A000207011 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AE20EE18227773A000207011 /* Main.storyboard */; }; AE20EE1D227773A000207011 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AE20EE1C227773A000207011 /* main.m */; }; AE95B97A227CBE5A0038195A /* piet_metal.h in Headers */ = {isa = PBXBuildFile; fileRef = AE95B979227CBE590038195A /* piet_metal.h */; }; AE95B981227CC0020038195A /* libpiet_metal.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AE95B975227CBDCD0038195A /* libpiet_metal.a */; }; AE99FF612277800100937601 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AE99FF5E2277800100937601 /* README.md */; }; AE99FF652277809B00937601 /* PietRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = AE99FF642277809B00937601 /* PietRenderer.m */; }; AE99FF692277CE7200937601 /* PietRender.metal in Sources */ = {isa = PBXBuildFile; fileRef = AE99FF682277CE7200937601 /* PietRender.metal */; }; AEA81C9C227A698E0051DAD5 /* SceneEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = AEA81C9B227A698E0051DAD5 /* SceneEncoder.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ AE95B97F227CBFEA0038195A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AE20EE052277739F00207011 /* Project object */; proxyType = 1; remoteGlobalIDString = AE95B974227CBDCD0038195A; remoteInfo = piet_metal; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ AE20EE0D2277739F00207011 /* piet-metal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "piet-metal.app"; sourceTree = BUILT_PRODUCTS_DIR; }; AE20EE102277739F00207011 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; AE20EE112277739F00207011 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; AE20EE132277739F00207011 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; AE20EE142277739F00207011 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; AE20EE16227773A000207011 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AE20EE19227773A000207011 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; AE20EE1B227773A000207011 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AE20EE1C227773A000207011 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; AE20EE1E227773A000207011 /* piet_metal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = piet_metal.entitlements; sourceTree = ""; }; AE340C0923C546BF00F9D470 /* GenTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GenTypes.h; sourceTree = ""; }; AE95B96F227CBC1B0038195A /* libpiet_metal.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libpiet_metal.a; path = target/debug/libpiet_metal.a; sourceTree = ""; }; AE95B975227CBDCD0038195A /* libpiet_metal.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libpiet_metal.a; sourceTree = BUILT_PRODUCTS_DIR; }; AE95B979227CBE590038195A /* piet_metal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = piet_metal.h; path = include/piet_metal.h; sourceTree = ""; }; AE99FF5E2277800100937601 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; AE99FF642277809B00937601 /* PietRenderer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PietRenderer.m; sourceTree = ""; }; AE99FF66227780B000937601 /* PietRenderer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PietRenderer.h; sourceTree = ""; }; AE99FF672277CDF200937601 /* PietShaderTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PietShaderTypes.h; sourceTree = ""; }; AE99FF682277CE7200937601 /* PietRender.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = PietRender.metal; sourceTree = ""; }; AEA81C9B227A698E0051DAD5 /* SceneEncoder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneEncoder.m; sourceTree = ""; }; AEA81C9D227A69B50051DAD5 /* SceneEncoder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneEncoder.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ AE20EE0A2277739F00207011 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( AE95B981227CC0020038195A /* libpiet_metal.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ AE20EE042277739F00207011 = { isa = PBXGroup; children = ( AE95B979227CBE590038195A /* piet_metal.h */, AE99FF5E2277800100937601 /* README.md */, AE20EE0F2277739F00207011 /* TestApp */, AE20EE0E2277739F00207011 /* Products */, AE95B96E227CBC1A0038195A /* Frameworks */, ); sourceTree = ""; }; AE20EE0E2277739F00207011 /* Products */ = { isa = PBXGroup; children = ( AE20EE0D2277739F00207011 /* piet-metal.app */, AE95B975227CBDCD0038195A /* libpiet_metal.a */, ); name = Products; sourceTree = ""; }; AE20EE0F2277739F00207011 /* TestApp */ = { isa = PBXGroup; children = ( AE20EE102277739F00207011 /* AppDelegate.h */, AE20EE112277739F00207011 /* AppDelegate.m */, AE20EE16227773A000207011 /* Assets.xcassets */, AE20EE1B227773A000207011 /* Info.plist */, AE20EE1C227773A000207011 /* main.m */, AE20EE18227773A000207011 /* Main.storyboard */, AE20EE1E227773A000207011 /* piet_metal.entitlements */, AE99FF682277CE7200937601 /* PietRender.metal */, AE99FF66227780B000937601 /* PietRenderer.h */, AE99FF642277809B00937601 /* PietRenderer.m */, AE99FF672277CDF200937601 /* PietShaderTypes.h */, AEA81C9D227A69B50051DAD5 /* SceneEncoder.h */, AEA81C9B227A698E0051DAD5 /* SceneEncoder.m */, AE20EE132277739F00207011 /* ViewController.h */, AE20EE142277739F00207011 /* ViewController.m */, AE340C0923C546BF00F9D470 /* GenTypes.h */, ); path = TestApp; sourceTree = ""; }; AE95B96E227CBC1A0038195A /* Frameworks */ = { isa = PBXGroup; children = ( AE95B96F227CBC1B0038195A /* libpiet_metal.a */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ AE95B971227CBDCD0038195A /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( AE95B97A227CBE5A0038195A /* piet_metal.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ AE20EE0C2277739F00207011 /* piet-metal */ = { isa = PBXNativeTarget; buildConfigurationList = AE20EE21227773A000207011 /* Build configuration list for PBXNativeTarget "piet-metal" */; buildPhases = ( AE20EE092277739F00207011 /* Sources */, AE20EE0A2277739F00207011 /* Frameworks */, AE20EE0B2277739F00207011 /* Resources */, ); buildRules = ( ); dependencies = ( AE95B980227CBFEA0038195A /* PBXTargetDependency */, ); name = "piet-metal"; productName = "piet-metal"; productReference = AE20EE0D2277739F00207011 /* piet-metal.app */; productType = "com.apple.product-type.application"; }; AE95B974227CBDCD0038195A /* piet_metal */ = { isa = PBXNativeTarget; buildConfigurationList = AE95B976227CBDCD0038195A /* Build configuration list for PBXNativeTarget "piet_metal" */; buildPhases = ( AE95B971227CBDCD0038195A /* Headers */, AE95B97B227CBE660038195A /* ShellScript */, ); buildRules = ( ); dependencies = ( ); name = piet_metal; productName = piet_metal; productReference = AE95B975227CBDCD0038195A /* libpiet_metal.a */; productType = "com.apple.product-type.library.static"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ AE20EE052277739F00207011 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1020; ORGANIZATIONNAME = "Raph Levien"; TargetAttributes = { AE20EE0C2277739F00207011 = { CreatedOnToolsVersion = 10.2.1; }; AE95B974227CBDCD0038195A = { CreatedOnToolsVersion = 10.2.1; }; }; }; buildConfigurationList = AE20EE082277739F00207011 /* Build configuration list for PBXProject "piet-metal" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = AE20EE042277739F00207011; productRefGroup = AE20EE0E2277739F00207011 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( AE20EE0C2277739F00207011 /* piet-metal */, AE95B974227CBDCD0038195A /* piet_metal */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ AE20EE0B2277739F00207011 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( AE99FF612277800100937601 /* README.md in Resources */, AE20EE17227773A000207011 /* Assets.xcassets in Resources */, AE20EE1A227773A000207011 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ AE95B97B227CBE660038195A /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# When building from Xcode we want to ensure that `cargo` is in PATH.\n# as a convenience, add the default cargo install location\nexport PATH=\"$PATH:${HOME}/.cargo/bin\"\n\nset -e\n\nif [[ $CONFIGURATION = \"Debug\" ]]; then\nRUST_CONFIGURATION=\"debug\"\nRUST_CONFIGURATION_FLAG=\"\"\nelse\nRUST_CONFIGURATION=\"release\"\nRUST_CONFIGURATION_FLAG=\"--release\"\nfi\n\ncd \"${SRCROOT}\"\n\necho \"rust config ${RUST_CONFIGURATION}, action ${ACTION}\"\n\nif [[ ${ACTION:-build} = \"build\" ]]; then\ncargo build $RUST_CONFIGURATION_FLAG\ncp \"${SRCROOT}/target/${RUST_CONFIGURATION}/libpiet_metal.a\" \"${BUILT_PRODUCTS_DIR}/\"\n#cp \"${SRCROOT}/include/piet_metal.h\" \"${BUILT_PRODUCTS_DIR}/\"\nelif [[ $ACTION = \"clean\" ]]; then\necho \"cleaning\"\ncargo clean\nfi\n\nset +e\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ AE20EE092277739F00207011 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( AE20EE152277739F00207011 /* ViewController.m in Sources */, AE20EE1D227773A000207011 /* main.m in Sources */, AE99FF652277809B00937601 /* PietRenderer.m in Sources */, AEA81C9C227A698E0051DAD5 /* SceneEncoder.m in Sources */, AE99FF692277CE7200937601 /* PietRender.metal in Sources */, AE20EE122277739F00207011 /* AppDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ AE95B980227CBFEA0038195A /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AE95B974227CBDCD0038195A /* piet_metal */; targetProxy = AE95B97F227CBFEA0038195A /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ AE20EE18227773A000207011 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( AE20EE19227773A000207011 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ AE20EE1F227773A000207011 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; }; name = Debug; }; AE20EE20227773A000207011 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; }; name = Release; }; AE20EE22227773A000207011 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = TestApp/piet_metal.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = TestApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/target/debug", ); PRODUCT_BUNDLE_IDENTIFIER = "com.levien.piet-metal"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; AE20EE23227773A000207011 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = TestApp/piet_metal.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = TestApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/target/debug", ); PRODUCT_BUNDLE_IDENTIFIER = "com.levien.piet-metal"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; AE95B977227CBDCD0038195A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; EXECUTABLE_PREFIX = lib; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; name = Debug; }; AE95B978227CBDCD0038195A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; EXECUTABLE_PREFIX = lib; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ AE20EE082277739F00207011 /* Build configuration list for PBXProject "piet-metal" */ = { isa = XCConfigurationList; buildConfigurations = ( AE20EE1F227773A000207011 /* Debug */, AE20EE20227773A000207011 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AE20EE21227773A000207011 /* Build configuration list for PBXNativeTarget "piet-metal" */ = { isa = XCConfigurationList; buildConfigurations = ( AE20EE22227773A000207011 /* Debug */, AE20EE23227773A000207011 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AE95B976227CBDCD0038195A /* Build configuration list for PBXNativeTarget "piet_metal" */ = { isa = XCConfigurationList; buildConfigurations = ( AE95B977227CBDCD0038195A /* Debug */, AE95B978227CBDCD0038195A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = AE20EE052277739F00207011 /* Project object */; } ================================================ FILE: piet-metal.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: piet-metal.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: piet-metal.xcodeproj/xcuserdata/raph.xcuserdatad/xcschemes/xcschememanagement.plist ================================================ SchemeUserState libpiet_metal.xcscheme_^#shared#^_ orderHint 1 libpietmetal.xcscheme_^#shared#^_ orderHint 1 piet-metal.xcscheme_^#shared#^_ orderHint 0 piet_metal.xcscheme_^#shared#^_ orderHint 1 ================================================ FILE: src/flatten.rs ================================================ // Copyright 2019 The xi-editor authors. //! Quick and dirty path flattening. // A proper path flattening algorithm belongs in kurbo. In the meantime, this will let us // get something rendered. use kurbo::{BezPath, CubicBez, PathEl, Point}; pub fn flatten_path(path: &BezPath, tolerance: f64) -> Vec> { let mut result: Vec> = Vec::new(); let mut cur_path = None; let mut last_pt = Point::default(); for el in path.elements() { match el { PathEl::MoveTo(p) => { if let Some(sp) = cur_path.take() { result.push(sp); } cur_path = Some(vec![*p]); last_pt = *p; } PathEl::LineTo(p) => { cur_path.as_mut().unwrap().push(*p); last_pt = *p; } PathEl::CurveTo(p1, p2, p3) => { let cb = CubicBez::new(last_pt, *p1, *p2, *p3); // This is a really hacky way to get finer subdivision. It will // give overly coarse results if the Bézier is close to cubic. But // close enough for now. // // A reasonable approach would be to subdivide the quads based // on the true error, or we could try to do a fancier algorithm. for (_, _, q) in cb.to_quads(tolerance * 1e-2) { cur_path.as_mut().unwrap().push(q.p2); } last_pt = *p3; } _ => (), } } if let Some(sp) = cur_path.take() { result.push(sp); } result } ================================================ FILE: src/lib.rs ================================================ // Copyright 2019 The xi-editor authors. use std::mem; use std::ptr::copy_nonoverlapping; use std::str::FromStr; use kurbo::{BezPath, Circle, Line, Point, Rect, Shape, Vec2}; use roxmltree::Document; mod flatten; // Keep these in sync with PietShaderTypes.h #[repr(C)] #[derive(Clone, Copy)] struct SimpleGroup { n_items: u32, items_ix: u32, } #[repr(C)] #[derive(Clone, Copy, Default)] struct ShortBbox([u16; 4]); #[repr(C)] union PietItem { circle: PietCircle, stroke_line: PietStrokeLine, fill: PietFill, } #[repr(C)] #[derive(Clone, Copy)] struct PietCircle { item_type: ItemType, } #[repr(C)] #[derive(Clone, Copy)] struct PietStrokeLine { item_type: ItemType, flags: u32, rgba: u32, width: f32, start: (f32, f32), end: (f32, f32), } #[repr(C)] #[derive(Clone, Copy)] struct PietFill { item_type: ItemType, flags: u32, rgba: u32, n_points: u32, points_ix: u32, } #[repr(C)] #[derive(Clone, Copy)] struct PietStrokePolyLine { item_type: ItemType, rgba: u32, width: f32, n_points: u32, points_ix: u32, } #[repr(u32)] #[derive(Clone, Copy)] enum ItemType { Circle = 1, Line = 2, Fill = 3, StrokePolyLine = 4, } pub struct Encoder<'a> { buf: &'a mut [u8], free_space: usize, group_count: usize, group_ix: usize, // Start index of currently open group. group_start: usize, } impl ShortBbox { fn from_rect(rect: Rect) -> ShortBbox { ShortBbox([ rect.x0.floor().max(0.0).min(65535.0) as u16, rect.y0.floor().max(0.0).min(65535.0) as u16, rect.x1.ceil().max(0.0).min(65535.0) as u16, rect.y1.ceil().max(0.0).min(65535.0) as u16, ]) } } fn point_to_f32s(point: Point) -> (f32, f32) { (point.x as f32, point.y as f32) } impl<'a> Encoder<'a> { pub fn new(buf: &mut [u8]) -> Encoder { Encoder { buf, free_space: 0, group_count: 0, group_start: 0, group_ix: 0, } } pub fn alloc(&mut self, size: usize) -> usize { let result = self.free_space; self.free_space += size; result } // It's probably better to do this without unsafety (after all, we're just creating bytes). // Probably the thing to do is write proc macros. pub unsafe fn write_struct(&mut self, ix: usize, s: &T) { let len = mem::size_of::(); //println!("writing {} bytes at {}", len, ix); copy_nonoverlapping( s as *const T as *const u8, self.buf[ix..ix + len].as_mut_ptr(), len, ); } pub fn begin_group(&mut self, n_items: usize) { let item_start = mem::size_of::() + n_items * mem::size_of::(); let total_size = item_start + n_items * mem::size_of::(); self.group_start = self.alloc(total_size); self.group_count = n_items; let group = SimpleGroup { n_items: n_items as u32, items_ix: (self.group_start + item_start) as u32, }; unsafe { self.write_struct(self.group_start, &group); } } pub fn end_group(&mut self) { assert_eq!(self.group_ix, self.group_count); // This will get more interesting when we have nested groups. } unsafe fn add_item(&mut self, item: &T, bbox: ShortBbox) { assert!(self.group_ix < self.group_count); let bbox_ix = self.group_start + mem::size_of::() + self.group_ix * mem::size_of::(); self.write_struct(bbox_ix, &bbox); let item_ix = self.group_start + mem::size_of::() + self.group_count * mem::size_of::() + self.group_ix * mem::size_of::(); self.write_struct(item_ix, item); self.group_ix += 1; } // Encode a circle. Currently this has a lot of limitations and is mostly used for debugging // and performance analysis, but could be expanded to the real thing. pub fn circle(&mut self, circle: &Circle) { let piet_circle = PietCircle { item_type: ItemType::Circle, }; unsafe { self.add_item(&piet_circle, ShortBbox::from_rect(circle.bounding_box())); } } // Should these be by reference or move? pub fn stroke_line(&mut self, line: Line, width: f32, rgba: u32) { let piet_stroke_line = PietStrokeLine { item_type: ItemType::Line, flags: Default::default(), rgba: rgba.to_be(), width, start: point_to_f32s(line.p0), end: point_to_f32s(line.p1), }; // TODO: do we need to add an additional 0.5? let hw = (width * 0.5) as f64; let bbox = line.bounding_box().inflate(hw, hw); unsafe { self.add_item(&piet_stroke_line, ShortBbox::from_rect(bbox)); } } // Signature will change, need to deal with subpaths and also want curves. pub fn fill(&mut self, points: &[Point], rgba: u32) { let (points_ix, bbox) = self.encode_points(points); let piet_fill = PietFill { item_type: ItemType::Fill, flags: Default::default(), rgba: rgba.to_be(), n_points: points.len() as u32, points_ix: points_ix as u32, }; unsafe { self.add_item(&piet_fill, ShortBbox::from_rect(bbox)); } } pub fn polyline(&mut self, points: &[Point], rgba: u32, width: f32) { let (points_ix, bbox) = self.encode_points(points); let piet_poly = PietStrokePolyLine { item_type: ItemType::StrokePolyLine, rgba: rgba.to_be(), width, n_points: points.len() as u32, points_ix: points_ix as u32, }; let hw = (width * 0.5) as f64; unsafe { self.add_item(&piet_poly, ShortBbox::from_rect(bbox.inflate(hw, hw))); } } pub fn encode_points(&mut self, points: &[Point]) -> (usize, Rect) { let points_ix = self.alloc(points.len() * mem::size_of::<(f32, f32)>()); let mut dst = points_ix; let mut bbox = None; for &pt in points { bbox = match bbox { None => Some(Rect::from_points(pt, pt)), Some(old_bbox) => Some(old_bbox.union_pt(pt)), }; unsafe { self.write_struct(dst, &point_to_f32s(pt)); dst += mem::size_of::<(f32, f32)>(); } } let bbox = bbox.expect("encoded empty points vector"); (points_ix, bbox) } #[allow(unused)] fn debug_print(&self) { unsafe { for i in (0..self.free_space).step_by(4) { println!( "{:04x}: {:08x}", i, std::ptr::read((self.buf.as_ptr().add(i) as *const u32)) ); } } } } #[allow(unused)] fn make_cardioid(encoder: &mut Encoder) { let n = 97; let dth = std::f64::consts::PI * 2.0 / (n as f64); let center = Point::new(1024.0, 768.0); let r = 750.0; encoder.begin_group((n - 1) * 2); for i in 1..n { let p0 = center + Vec2::from_angle(i as f64 * dth) * r; let p1 = center + Vec2::from_angle(((i * 2) % n) as f64 * dth) * r; encoder.circle(&Circle::new(p0, 8.0)); encoder.stroke_line(Line::new(p0, p1), 2.0, 0x000080e0); } encoder.end_group(); } #[allow(unused)] fn make_path_test(encoder: &mut Encoder) { encoder.begin_group(1); encoder.fill( &[ Point::new(10.0, 10.0), Point::new(15.0, 800.0), Point::new(300.0, 500.0), ], 0x80e0, ); encoder.end_group(); } fn make_tiger(encoder: &mut Encoder) { let scale = 8.0; let tiger_svg = include_bytes!("../Ghostscript_Tiger.svg"); let doc = Document::parse(std::str::from_utf8(tiger_svg).unwrap()).unwrap(); let root = doc.root_element(); let g = root.first_element_child().unwrap(); let mut n_items = 0; for path in g.children() { if path.is_element() { let d = path.attribute("d").unwrap(); if let Ok(ref bp) = BezPath::from_svg(d) { let xform_path = kurbo::Affine::scale(scale) * bp; if path.attribute("fill").is_some() { n_items += count_fill_items(&xform_path); } if path.attribute("stroke").is_some() { n_items += count_stroke_items(&xform_path); } } } } println!("{} items", n_items); encoder.begin_group(n_items); for path in g.children() { if path.is_element() { let d = path.attribute("d").unwrap(); let bez_path = BezPath::from_svg(d); if let Ok(ref bp) = bez_path { let xform_path = kurbo::Affine::scale(scale) * bp; if let Some(fill_color) = path.attribute("fill") { encode_path(encoder, &xform_path, parse_color(fill_color)); } if let Some(stroke_color) = path.attribute("stroke") { let width = f32::from_str(path.attribute("stroke-width").unwrap()).unwrap(); let width = width * (scale as f32); let color = parse_color(stroke_color); encode_path_stroke(encoder, &xform_path, width, color); } } } } encoder.end_group(); } const TOLERANCE: f64 = 0.1; fn count_fill_items(bezpath: &BezPath) -> usize { let flattened = flatten::flatten_path(bezpath, TOLERANCE); flattened.len() } fn count_stroke_items(bezpath: &BezPath) -> usize { let flattened = flatten::flatten_path(bezpath, TOLERANCE); flattened.len() } fn encode_path(encoder: &mut Encoder, bezpath: &BezPath, rgba: u32) { let flattened = flatten::flatten_path(bezpath, TOLERANCE); for subpath in &flattened { encoder.fill(subpath, rgba); } } // This is a tradeoff between smoothness and contrast, set by aesthetic preference. The // optimum rendering of very thin strokes is likely an area for further research. const THIN_LINE: f32 = 0.7; fn encode_path_stroke(encoder: &mut Encoder, bezpath: &BezPath, mut width: f32, mut rgba: u32) { // Fudge very thin lines to get better distance field rendering. if width < THIN_LINE { let alpha = (rgba & 0xff) as f32; // The sqrt here is to compensate for "correct" alpha blending. // We probably want a more systematic approach to stroke thickening. let alpha = alpha * (width / THIN_LINE).sqrt(); rgba = (rgba & !0xff) | (alpha as u32); width = THIN_LINE; } let flattened = flatten::flatten_path(bezpath, TOLERANCE); for subpath in &flattened { encoder.polyline(subpath, rgba, width); } } fn make_test_scene(encoder: &mut Encoder) { //make_cardioid(encoder); //make_path_test(encoder); make_tiger(encoder); } fn parse_color(color: &str) -> u32 { if color.as_bytes()[0] == b'#' { let mut hex = u32::from_str_radix(&color[1..], 16).unwrap(); if color.len() == 4 { hex = (hex >> 8) * 0x110000 + ((hex >> 4) & 0xf) * 0x1100 + (hex & 0xf) * 0x11; } (hex << 8) + 0xff } else { 0xff00ff80 } } #[no_mangle] pub unsafe extern "C" fn init_test_scene(scene_buf: *mut u8, buf_size: usize) { let buf_slice = std::slice::from_raw_parts_mut(scene_buf, buf_size); let mut encoder = Encoder::new(buf_slice); make_test_scene(&mut encoder); //encoder.debug_print(); } ================================================ FILE: src/main.rs ================================================ #[macro_use] extern crate piet_gpu_derive; //#[derive(PietMetal)] struct SimpleGroup { n_items: u32, items_ix: u32, // TODO: bbox } piet_gpu! { mod scene { struct SimpleGroup { n_items: u32, // This should actually be a variable size array. items_ix: Ref, // Note: we want a variable size array of bboxes bbox: [u16; 4], } struct PietCircle { } struct PietStrokeLine { flags: u32, rgba_color: u32, width: f32, start: [f32; 2], end: [f32; 2], } struct PietFill { flags: u32, rgba_color: u32, n_points: u32, points_ix: Ref, } struct PietStrokePolyLine { rgba_color: u32, width: f32, n_points: u32, points_ix: Ref, } enum PietItem { Circle(PietCircle), Line(PietStrokeLine), Fill(PietFill), Poly(PietStrokePolyLine), } } } piet_gpu! { mod ptcl { struct CmdCircle { // In existing code, this is packed; we might need an annotation for this. bbox: [u16; 4], } struct CmdLine { start: [f32; 2], end: [f32; 2], } struct CmdStroke { // In existing code, this is f16. Should we have support? halfWidth: f32, rgba_color: u32, } struct CmdFill { start: [f32; 2], end: [f32; 2], } struct CmdFillEdge { // The sign is only one bit. sign: i32, y: f32, } struct CmdDrawFill { backdrop: i32, rgba_color: u32, } struct CmdSolid { rgba_color: u32, } enum Cmd { End, Circle(CmdCircle), Line(CmdLine), Fill(CmdFill), Stroke(CmdStroke), FillEdge(CmdFillEdge), DrawFill(CmdDrawFill), Solid(CmdSolid), Bail, } } } fn main() { print!("{}", gen_gpu_scene("MSL")); }