├── demo.png ├── tiger.png ├── screenshot.png ├── test.sh ├── .spi.yml ├── Sources ├── vger │ ├── stb_rect_pack.cpp │ ├── fonts │ │ └── Anodina-Regular.ttf │ ├── Interval.h │ ├── bezier.h │ ├── vgerGlyphPathCache.h │ ├── vgerGlyphCache.h │ ├── metal_compat.h │ ├── vgerRenderer.h │ ├── vgerTextureManager.h │ ├── path.metal │ ├── paint.h │ ├── vgerPathScanner.h │ ├── prim.h │ ├── vgerScene.h │ ├── vgerGlyphPathCache.mm │ ├── vgerTextureManager.mm │ ├── vgerPathScanner.mm │ ├── vgerGlyphCache.mm │ ├── vgerRenderer.mm │ ├── vger.metal │ ├── vger_private.h │ ├── include │ │ └── vger.h │ ├── sdf.h │ └── stb_rect_pack.h └── vgerSwift │ ├── TextureHandle.swift │ ├── vgerSwift.docc │ └── vgerSwift.md │ ├── MTKView+ContentScaleFactor.swift │ ├── TextureRenderer.metal │ ├── Renderer.swift │ ├── TextureRenderer.swift │ └── VgerView.swift ├── Tests └── vgerTests │ ├── images │ ├── demo.png │ ├── rects.png │ ├── text.png │ ├── xform.png │ ├── rotate.png │ ├── icon-mac-128.png │ ├── icon-mac-16.png │ ├── icon-mac-256.png │ ├── icon-mac-32.png │ ├── icon-mac-64.png │ ├── text_scaled.png │ └── vger_basics.png │ ├── link.m │ ├── testUtils.h │ ├── vgerGlyphCacheTests.mm │ ├── vgerPathScannerTests.mm │ ├── vgerTextureManagerTests.mm │ ├── sdfTests.mm │ ├── testUtils.mm │ └── vgerTests.mm ├── Demo ├── Shared │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── nanosvg.m │ ├── VgerDemo (iOS)-Bridging-Header.h │ ├── VgerDemo (macOS)-Bridging-Header.h │ ├── Triangle.svg │ ├── VgerDemoApp.swift │ ├── ContentView.swift │ ├── HelloView.swift │ ├── DemoView.swift │ └── TigerView.swift ├── README.md ├── VgerDemo.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── project.pbxproj ├── macOS │ ├── macOS.entitlements │ └── Info.plist ├── LICENSE └── iOS │ └── Info.plist ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── metallib.sh ├── Package.swift ├── .github └── workflows │ └── build.yml ├── LICENSE ├── .gitignore ├── CLAUDE.md └── README.md /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/demo.png -------------------------------------------------------------------------------- /tiger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/tiger.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/screenshot.png -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | xcodebuild test -scheme vger -sdk macosx -destination "name=My Mac" 2 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [vgerSwift] 5 | -------------------------------------------------------------------------------- /Sources/vger/stb_rect_pack.cpp: -------------------------------------------------------------------------------- 1 | 2 | #define STB_RECT_PACK_IMPLEMENTATION 1 3 | #include "stb_rect_pack.h" 4 | -------------------------------------------------------------------------------- /Tests/vgerTests/images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/demo.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/rects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/rects.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/text.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/xform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/xform.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/rotate.png -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/vger/fonts/Anodina-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Sources/vger/fonts/Anodina-Regular.ttf -------------------------------------------------------------------------------- /Tests/vgerTests/images/icon-mac-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/icon-mac-128.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/icon-mac-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/icon-mac-16.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/icon-mac-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/icon-mac-256.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/icon-mac-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/icon-mac-32.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/icon-mac-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/icon-mac-64.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/text_scaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/text_scaled.png -------------------------------------------------------------------------------- /Tests/vgerTests/images/vger_basics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/audulus/vger/HEAD/Tests/vgerTests/images/vger_basics.png -------------------------------------------------------------------------------- /Demo/README.md: -------------------------------------------------------------------------------- 1 | # vger-demo 2 | iOS / macOS demo of vger 3 | 4 | ![build status](https://github.com/audulus/vger/actions/workflows/build.yml/badge.svg) 5 | -------------------------------------------------------------------------------- /Demo/Shared/nanosvg.m: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import 4 | 5 | #define NANOSVG_IMPLEMENTATION 6 | #include "nanosvg.h" 7 | -------------------------------------------------------------------------------- /Demo/Shared/VgerDemo (iOS)-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #include "nanosvg.h" 6 | -------------------------------------------------------------------------------- /Demo/Shared/VgerDemo (macOS)-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #include "nanosvg.h" 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Shared/Triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Demo/VgerDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/vgerTests/link.m: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | @import MetalKit; 4 | @import XCTest; 5 | @import ImageIO; 6 | #if !TARGET_OS_OSX 7 | @import MobileCoreServices; 8 | #endif 9 | @import UniformTypeIdentifiers; 10 | -------------------------------------------------------------------------------- /Demo/Shared/VgerDemoApp.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | import SwiftUI 4 | 5 | @main 6 | struct VgerDemoApp: App { 7 | var body: some Scene { 8 | WindowGroup { 9 | ContentView() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/VgerDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/macOS/macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo/Shared/ContentView.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | import SwiftUI 4 | 5 | struct ContentView: View { 6 | @StateObject var model = TigerModel() 7 | var body: some View { 8 | TigerView(model: model) 9 | } 10 | } 11 | 12 | struct ContentView_Previews: PreviewProvider { 13 | static var previews: some View { 14 | ContentView() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/vger/Interval.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | struct Interval { 6 | 7 | float a; 8 | float b; 9 | 10 | Interval() : a(0), b(0) { } 11 | Interval(float a, float b) : a(a), b(b) { } 12 | 13 | bool empty() const { return a > b; } 14 | 15 | bool intersects(Interval I) const 16 | { 17 | return b > I.a and a < I.b; 18 | } 19 | 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /Demo/Shared/HelloView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import vger 3 | import vgerSwift 4 | 5 | struct HelloView: View { 6 | let cyan = SIMD4(0,1,1,1) 7 | 8 | var body: some View { 9 | VgerView { vger, _ in 10 | vgerText(vger, "Hello world. This is V'Ger.", cyan, 0) 11 | } 12 | } 13 | } 14 | 15 | struct HelloView_Previews: PreviewProvider { 16 | static var previews: some View { 17 | HelloView() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/vgerSwift/TextureHandle.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | import vger 4 | 5 | public class TextureHandle { 6 | public let vger: vgerContext 7 | public let textureIndex: vgerImageIndex 8 | 9 | public init(vger: vgerContext, index: vgerImageIndex) { 10 | self.vger = vger 11 | self.textureIndex = index 12 | } 13 | 14 | deinit { 15 | vgerDeleteTexture(vger, textureIndex) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/vger/bezier.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | using namespace simd; 7 | 8 | // See https://ttnghia.github.io/pdf/QuadraticApproximation.pdf 9 | 10 | // Approximate cubic bezier with two quadratics. 11 | void approx_cubic(float2 b[4], float2 q[6]) { 12 | 13 | q[0] = b[0]; 14 | q[5] = b[3]; 15 | 16 | q[1] = simd_mix(b[0], b[1], 0.75); 17 | q[4] = simd_mix(b[2], b[3], 0.25); 18 | 19 | q[2] = q[3] = simd_mix(q[1], q[4], 0.5); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /metallib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | set -euo pipefail 3 | 4 | # This script builds a metallib suitable for debugging. Unfortunately, SPM doesn't support compiling metal with debug symbols on. 5 | # See https://forums.swift.org/t/cant-profile-metal-shaders-within-a-package/49607 6 | 7 | export MTL_ENABLE_DEBUG_INFO=INCLUDE_SOURCE 8 | export SDKROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk 9 | 10 | xcrun --sdk macosx metal -target air64-apple-macos12.1 -ffast-math -gline-tables-only -MO -o vger.metallib Sources/vger/vger.metal 11 | -------------------------------------------------------------------------------- /Tests/vgerTests/testUtils.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #ifndef TestUtils_h 4 | #define TestUtils_h 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | void writeCGImage(CGImageRef image, CFURLRef url); 11 | CGImageRef createImage(UInt8* data, int w, int h); 12 | CGImageRef createImage(id texture); 13 | void showTexture(id texture, NSString* name); 14 | bool checkTexture(id texture, NSString* name); 15 | 16 | #endif /* TestUtils_h */ 17 | -------------------------------------------------------------------------------- /Sources/vgerSwift/vgerSwift.docc/vgerSwift.md: -------------------------------------------------------------------------------- 1 | # ``vgerSwift`` 2 | 3 | Vector graphics renderer for dynamic UIs. 4 | 5 | ## Overview 6 | 7 | vger is a vector graphics renderer which renders a limited set of primitives, but does so almost entirely on the GPU. Works on iOS and macOS. API is plain C. 8 | 9 | Each primitive can be filled with a solid color, gradient, or texture. vger renders primitives as instanced quads, with most of the calculations done in the fragment shader. 10 | 11 | ## Topics 12 | 13 | ### Group 14 | 15 | - ``Symbol`` 16 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "vger", 7 | platforms: [.macOS(.v11), .iOS(.v14)], 8 | products: [.library(name: "vger", targets: ["vger", "vgerSwift"])], 9 | dependencies: [.package(url: "https://github.com/wtholliday/MetalNanoVG", branch: "spm")], 10 | targets: [ 11 | .target(name: "vger", dependencies: [], resources: [.copy("fonts")]), 12 | .target(name: "vgerSwift", dependencies: ["vger"]), 13 | .testTarget(name: "vgerTests", dependencies: ["vger", "MetalNanoVG"], resources: [.copy("images")]), 14 | ], 15 | cxxLanguageStandard: .cxx20 16 | ) 17 | -------------------------------------------------------------------------------- /Sources/vger/vgerGlyphPathCache.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include "vger.h" 6 | #include "vgerPathScanner.h" 7 | #include 8 | #include 9 | #import 10 | #import 11 | #import "prim.h" 12 | 13 | struct vgerGlyphPathCache { 14 | 15 | struct Info { 16 | std::vector cvs; 17 | std::vector prims; 18 | }; 19 | 20 | std::unordered_map _cache; 21 | 22 | CTFontRef ctFont; 23 | 24 | vgerPathScanner scan; 25 | 26 | vgerGlyphPathCache(); 27 | ~vgerGlyphPathCache(); 28 | 29 | Info& getInfo(CGGlyph); 30 | }; 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | env: 5 | XCODE_VER: 12.4 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-latest 10 | steps: 11 | - name: Check out vger 12 | uses: actions/checkout@v4.1.1 13 | - name: Build vger 14 | run: | 15 | set -euo pipefail 16 | xcodebuild -scheme vger -destination 'platform=OS X,arch=x86_64' clean build | xcpretty 17 | 18 | demo: 19 | runs-on: macos-latest 20 | steps: 21 | - name: Check out vger-demo 22 | uses: actions/checkout@v4.1.1 23 | - name: Build vger-demo 24 | run: | 25 | set -euo pipefail 26 | cd Demo 27 | xcodebuild -scheme "VgerDemo (macOS)" -destination 'platform=OS X,arch=x86_64' clean build | xcpretty 28 | 29 | -------------------------------------------------------------------------------- /Sources/vgerSwift/MTKView+ContentScaleFactor.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | import Foundation 4 | import MetalKit 5 | 6 | #if os(macOS) 7 | 8 | /// Shim contentScaleFactor into MTKView on macOS. 9 | extension MTKView { 10 | 11 | var contentScaleFactor: CGFloat { 12 | get { 13 | return layer!.contentsScale 14 | } 15 | set { 16 | if let metalLayer = layer as? CAMetalLayer { 17 | metalLayer.contentsScale = newValue 18 | drawableSize = 19 | CGSize(width: metalLayer.bounds.size.width * newValue, 20 | height: metalLayer.bounds.size.height * newValue) 21 | } 22 | } 23 | } 24 | 25 | func setNeedsDisplay() { 26 | super.setNeedsDisplay(self.frame) 27 | } 28 | 29 | } 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/vgerSwift/TextureRenderer.metal: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Halfspace LLC. All rights reserved. 2 | 3 | #include 4 | using namespace metal; 5 | 6 | struct VertexOut { 7 | float4 position [[ position ]]; 8 | float2 t; 9 | }; 10 | 11 | constant float2 verts[4] = { float2(-1, -1), float2(1, -1), float2(-1, 1), float2(1, 1) }; 12 | 13 | vertex VertexOut textureVertex(uint vid [[ vertex_id ]]) { 14 | 15 | VertexOut out; 16 | out.position = float4(verts[vid], 0.0, 1.0); 17 | out.t = (verts[vid] + float2(1)) * .5; 18 | out.t.y = 1.0 - out.t.y; 19 | return out; 20 | 21 | } 22 | 23 | constexpr sampler s(coord::normalized, 24 | filter::nearest); 25 | 26 | fragment half4 textureFragment(VertexOut in [[ stage_in ]], 27 | texture2d tex) { 28 | 29 | return half4(tex.sample(s, in.t)); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Demo/macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/vger/vgerGlyphCache.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import 4 | #include 5 | #import 6 | #import 7 | #import 8 | #include "stb_rect_pack.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | #define GLYPH_MARGIN 4 13 | 14 | struct GlyphInfo { 15 | float size = 0.0; 16 | int regionIndex = -1; 17 | int textureWidth = 0; 18 | int textureHeight = 0; 19 | CGRect glyphBounds = CGRectZero; 20 | }; 21 | 22 | @interface vgerGlyphCache : NSObject 23 | 24 | @property (nonatomic, readonly) float usage; 25 | 26 | - (instancetype)initWithDevice:(id) device; 27 | 28 | - (GlyphInfo) getGlyph:(CGGlyph)glyph scale:(float)scale; 29 | 30 | - (void) update:(id) buffer; 31 | 32 | - (id) getAltas; 33 | 34 | /// Get a pointer to the first rectangle. 35 | - (stbrp_rect*) getRects; 36 | 37 | - (CTFontRef) getFont; 38 | 39 | @end 40 | 41 | NS_ASSUME_NONNULL_END 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Audulus LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Demo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Audulus LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/vger/metal_compat.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #ifdef __METAL_VERSION__ 6 | #define DEVICE device 7 | 8 | #else 9 | #define DEVICE 10 | 11 | #include 12 | using namespace simd; 13 | 14 | inline float min(float a, float b) { 15 | return a > b ? b : a; 16 | } 17 | 18 | inline float max(float a, float b) { 19 | return a > b ? a : b; 20 | } 21 | 22 | inline float2 max(float2 a, float b) { 23 | return simd_max(a, float2{b,b}); 24 | } 25 | 26 | inline float clamp(float x, float a, float b) { 27 | if(x > b) x = b; 28 | if(x < a) x = a; 29 | return x; 30 | } 31 | 32 | inline float3 clamp(float3 x, float a, float b) { 33 | return simd_clamp(x, a, b); 34 | } 35 | 36 | inline float mix(float a, float b, float t) { 37 | return (1-t)*a + t*b; 38 | } 39 | 40 | inline float2 mix(float2 a, float2 b, float t) { 41 | return (1-t)*a + t*b; 42 | } 43 | 44 | inline float3 mix(float3 a, float3 b, float t) { 45 | return (1-t)*a + t*b; 46 | } 47 | 48 | inline float4 mix(float4 a, float4 b, float t) { 49 | return (1-t)*a + t*b; 50 | } 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /Sources/vger/vgerRenderer.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import 4 | #import 5 | #include 6 | #include "vger.h" 7 | #include "vgerScene.h" 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | @interface vgerRenderer : NSObject 12 | 13 | - (instancetype)initWithDevice:(id) device 14 | pixelFormat:(MTLPixelFormat) pixelFormat; 15 | 16 | /// Render a buffer of prims. 17 | /// @param buffer commnd buffer for encoding 18 | /// @param pass render pass info 19 | /// @param primBuffer buffer of vgerPrims 20 | /// @param n number of vgerPrims in buffer 21 | /// @param texture texture to sample for textured prims 22 | - (void) encodeTo:(id) buffer 23 | pass:(MTLRenderPassDescriptor*) pass 24 | scene:(const vgerScene&) scene 25 | count:(int)n 26 | layer:(int)layer 27 | textures:(NSArray>*)textures 28 | glyphTexture:(id)glyphTexture 29 | windowSize:(vector_float2)windowSize 30 | glow:(bool)glow; 31 | 32 | @end 33 | 34 | NS_ASSUME_NONNULL_END 35 | -------------------------------------------------------------------------------- /Sources/vger/vgerTextureManager.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import 4 | #import 5 | #include "stb_rect_pack.h" 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | @interface vgerTextureManager : NSObject 10 | 11 | @property (nonatomic, retain, readonly) id atlas; 12 | @property (nonatomic, readonly) float usage; 13 | 14 | - (instancetype)initWithDevice:(id) device pixelFormat:(MTLPixelFormat)format; 15 | 16 | /// Creates a new region in the texture. 17 | /// @param data RGBA texture data (8-bits per component) 18 | /// @param width texture width 19 | /// @param height texture height 20 | /// @returns region index 21 | - (int) addRegion: (const uint8_t*) data width:(int)width height:(int)height bytesPerRow:(NSUInteger)bytesPerRow; 22 | 23 | /// Add region for an already loaded texture. 24 | - (int) addRegion:(id)texture; 25 | 26 | /// Updates the atlas texture. 27 | /// @param buffer to encode blit commands 28 | - (void) update:(id) buffer; 29 | 30 | /// Get a pointer to the first rectangle. 31 | - (stbrp_rect*) getRects; 32 | 33 | @end 34 | 35 | NS_ASSUME_NONNULL_END 36 | -------------------------------------------------------------------------------- /Tests/vgerTests/vgerGlyphCacheTests.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import 4 | #import "../../Sources/vger/vgerGlyphCache.h" 5 | #import 6 | #import "testUtils.h" 7 | 8 | @interface vgerGlyphCacheTests : XCTestCase { 9 | id device; 10 | id queue; 11 | } 12 | 13 | @end 14 | 15 | @implementation vgerGlyphCacheTests 16 | 17 | - (void)setUp { 18 | device = MTLCreateSystemDefaultDevice(); 19 | queue = [device newCommandQueue]; 20 | } 21 | 22 | - (void)testGlyphAtlas { 23 | 24 | auto cache = [[vgerGlyphCache alloc] initWithDevice:device]; 25 | 26 | for(int i=0;i<100;++i) { 27 | [cache getGlyph:i scale:1.0f]; 28 | } 29 | 30 | id buf = [queue commandBuffer]; 31 | [cache update:buf]; 32 | 33 | auto atlas = [cache getAltas]; 34 | 35 | // Sync texture on macOS 36 | #if TARGET_OS_OSX 37 | auto blitEncoder = [buf blitCommandEncoder]; 38 | [blitEncoder synchronizeResource:atlas]; 39 | [blitEncoder endEncoding]; 40 | #endif 41 | 42 | [buf commit]; 43 | [buf waitUntilCompleted]; 44 | 45 | showTexture(atlas, @"glyph_atlas.png"); 46 | 47 | } 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /Sources/vger/path.metal: -------------------------------------------------------------------------------- 1 | // Copyright © 2025 Audulus LLC. All rights reserved. 2 | 3 | #include 4 | using namespace metal; 5 | 6 | // New experimental tile-buffer based path rendering. 7 | // Draw paths by flipping in/out using a triangle fan 8 | // then flipping using bezier regions. 9 | 10 | struct GBufferData { 11 | 12 | /// Inside or outside the path? 13 | bool sign [[raster_order_group(0)]]; 14 | 15 | /// Approx distance to path curve for AA. 16 | float distance [[raster_order_group(0)]]; 17 | }; 18 | 19 | struct GBufferStore { 20 | GBufferData data [[imageblock_data]]; 21 | }; 22 | 23 | struct VertexOut { 24 | float4 position [[ position ]]; 25 | float2 uv; 26 | }; 27 | 28 | void flip(thread bool& b) { 29 | b = !b; 30 | } 31 | 32 | fragment GBufferStore path_fragment(VertexOut in [[ stage_in ]], 33 | GBufferStore fragmentValues [[imageblock_data]]) { 34 | 35 | GBufferStore result; 36 | result.data.sign = fragmentValues.data.sign; 37 | float u = in.uv.x; 38 | float v = in.uv.y; 39 | 40 | if (isnan(u)) { 41 | // If we're rendering the triangle fan, flip the 42 | // whole triangle. 43 | flip(result.data.sign); 44 | } else { 45 | // Flip the sign only for the interior pixels. 46 | if(u*u - v < 0) { 47 | flip(result.data.sign); 48 | } 49 | } 50 | 51 | return result; 52 | } 53 | -------------------------------------------------------------------------------- /Sources/vger/paint.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include "metal_compat.h" 6 | 7 | enum vgerPaintType { 8 | vgerPaintTypeLinearGradient, 9 | vgerPaintTypeRadialGradient, 10 | vgerPaintTypeImagePattern, 11 | }; 12 | 13 | struct vgerPaint { 14 | 15 | vgerPaintType type; 16 | 17 | #ifdef __METAL_VERSION__ 18 | float3x3 xform; 19 | #else 20 | matrix_float3x3 xform; 21 | #endif 22 | 23 | float4 innerColor; 24 | 25 | float4 outerColor; 26 | 27 | /// Inner radius for radial gradients. 28 | float innerRadius; 29 | 30 | /// Inner radius for radial gradients. 31 | float outerRadius; 32 | 33 | /// Render into the glow layer? 34 | float glow; 35 | 36 | /// Image if we're texturing. 37 | int32_t image; 38 | 39 | /// Flip Y coordinate? 40 | bool flipY; 41 | 42 | }; 43 | 44 | inline float4 applyPaint(const DEVICE vgerPaint& paint, float2 p) { 45 | 46 | switch (paint.type) { 47 | case vgerPaintTypeLinearGradient: 48 | { 49 | float d = clamp((paint.xform * float3{p.x, p.y, 1.0}).x, 0.0, 1.0); 50 | return mix(paint.innerColor, paint.outerColor, d); 51 | } 52 | case vgerPaintTypeRadialGradient: 53 | { 54 | float d = clamp(length( (paint.xform * float3{p.x, p.y, 1.0}).xy), paint.innerRadius, paint.outerRadius); 55 | return mix(paint.innerColor, paint.outerColor, (d-paint.innerRadius) / (paint.outerRadius - paint.innerRadius)); 56 | } 57 | default: 58 | return 0; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Demo/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sources/vger/vgerPathScanner.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include "Interval.h" 9 | #import 10 | 11 | struct vgerPathScanner { 12 | 13 | struct Segment { 14 | vector_float2 cvs[3]; 15 | int next = -1; 16 | int previous = -1; 17 | 18 | Interval yInterval() const { 19 | return { 20 | // Fatten the interval slightly to prevent artifacts by 21 | // slightly missing a curve in a band. 22 | std::min(cvs[0].y, std::min(cvs[1].y, cvs[2].y)) - 1, 23 | std::max(cvs[0].y, std::max(cvs[1].y, cvs[2].y)) + 1 24 | }; 25 | } 26 | 27 | Interval xInterval() const { 28 | return { 29 | // Fatten the interval slightly to prevent artifacts by 30 | // slightly missing a curve in a band. 31 | std::min(cvs[0].x, std::min(cvs[1].x, cvs[2].x)) - 1, 32 | std::max(cvs[0].x, std::max(cvs[1].x, cvs[2].x)) + 1 33 | }; 34 | } 35 | }; 36 | 37 | struct Node { 38 | float coord; 39 | int seg; 40 | bool end; 41 | }; 42 | 43 | std::vector segments; 44 | std::vector nodes; 45 | int index = 0; // current node index 46 | Interval interval; 47 | int first = -1; // first active segment 48 | int activeCount = 0; 49 | vector_float2 start{0,0}; 50 | vector_float2 p{0,0}; 51 | 52 | void _init(); 53 | void begin(vector_float2* cvs, int count); 54 | 55 | // In case we want to render glphs with paths. 56 | void begin(CGPathRef path); 57 | 58 | bool next(); 59 | 60 | }; 61 | -------------------------------------------------------------------------------- /Sources/vger/prim.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | /// VGER supports simple primitive types. 6 | typedef enum { 7 | 8 | /// Filled circle. 9 | vgerCircle, 10 | 11 | /// Stroked arc. 12 | vgerArc, 13 | 14 | /// Rounded corner rectangle. 15 | vgerRect, 16 | 17 | /// Stroked rounded rectangle. 18 | vgerRectStroke, 19 | 20 | /// Single-segment quadratic bezier curve. 21 | vgerBezier, 22 | 23 | /// line segment 24 | vgerSegment, 25 | 26 | /// Multi-segment bezier curve. 27 | vgerCurve, 28 | 29 | /// Connection wire. See https://www.shadertoy.com/view/NdsXRl 30 | vgerWire, 31 | 32 | /// Text rendering. 33 | vgerGlyph, 34 | 35 | /// Path fills. 36 | vgerPathFill 37 | 38 | } vgerPrimType; 39 | 40 | /// Primitive rendered by the GPU. 41 | typedef struct { 42 | 43 | /// Type of primitive. 44 | vgerPrimType type; 45 | 46 | /// Stroke width. 47 | float width; 48 | 49 | /// Radius of circles. Corner radius for rounded rectangles. 50 | float radius; 51 | 52 | /// Control vertices. 53 | vector_float2 cvs[3]; 54 | 55 | /// Start of the control vertices, if they're in a separate buffer. 56 | uint32_t start; 57 | 58 | /// Number of control vertices (vgerCurve and vgerPathFill) 59 | uint16_t count; 60 | 61 | /// Index of paint applied to drawing region. 62 | uint32_t paint; 63 | 64 | /// Glyph region index. (used internally) 65 | uint32_t glyph; 66 | 67 | /// Index of transform applied to drawing region. 68 | uint32_t xform; 69 | 70 | /// Min and max coordinates of the quad we're rendering. 71 | vector_float2 quadBounds[2]; 72 | 73 | /// Min and max coordinates in texture space. 74 | vector_float2 texBounds[2]; 75 | 76 | } vgerPrim; 77 | -------------------------------------------------------------------------------- /Sources/vgerSwift/Renderer.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | import Metal 4 | import MetalKit 5 | import vger 6 | 7 | class Renderer: NSObject, MTKViewDelegate { 8 | 9 | var vg = vgerNew(0, .bgra8Unorm) 10 | var device: MTLDevice! 11 | var queue: MTLCommandQueue! 12 | var renderCallback : ((vgerContext, CGSize) -> Void)? 13 | 14 | static let MaxBuffers = 3 15 | private let inflightSemaphore = DispatchSemaphore(value: MaxBuffers) 16 | 17 | init(device: MTLDevice) { 18 | self.device = device 19 | queue = device.makeCommandQueue() 20 | } 21 | 22 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 23 | 24 | } 25 | 26 | func draw(in view: MTKView) { 27 | 28 | let size = view.frame.size 29 | let w = Float(size.width) 30 | let h = Float(size.height) 31 | let scale = Float(view.contentScaleFactor) 32 | 33 | if w == 0 || h == 0 { 34 | return 35 | } 36 | 37 | vgerBegin(vg, w, h, scale) 38 | 39 | // use semaphore to encode 3 frames ahead 40 | _ = inflightSemaphore.wait(timeout: DispatchTime.distantFuture) 41 | 42 | let commandBuffer = queue.makeCommandBuffer()! 43 | 44 | let semaphore = inflightSemaphore 45 | commandBuffer.addCompletedHandler { _ in 46 | semaphore.signal() 47 | } 48 | 49 | renderCallback?(vg!, size) 50 | 51 | if let renderPassDescriptor = view.currentRenderPassDescriptor, let currentDrawable = view.currentDrawable { 52 | vgerEncode(vg, commandBuffer, renderPassDescriptor) 53 | commandBuffer.present(currentDrawable) 54 | } 55 | commandBuffer.commit() 56 | } 57 | 58 | deinit { 59 | vgerDelete(vg) 60 | } 61 | 62 | } 63 | 64 | -------------------------------------------------------------------------------- /Sources/vgerSwift/TextureRenderer.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Halfspace LLC. All rights reserved. 2 | 3 | import Metal 4 | 5 | /// Renders a texture to the entire screen. 6 | final class TextureRenderer { 7 | 8 | private var pipelineState: MTLRenderPipelineState! 9 | private var depthState: MTLDepthStencilState! 10 | 11 | init(device: MTLDevice, pixelFormat: MTLPixelFormat) { 12 | 13 | do { 14 | 15 | let lib = try device.makeDefaultLibrary(bundle: Bundle.module) 16 | let fragmentProgram = lib.makeFunction(name: "textureFragment")! 17 | let vertexProgram = lib.makeFunction(name: "textureVertex")! 18 | 19 | let pipelineStateDescriptor = MTLRenderPipelineDescriptor() 20 | pipelineStateDescriptor.vertexFunction = vertexProgram 21 | pipelineStateDescriptor.fragmentFunction = fragmentProgram 22 | pipelineStateDescriptor.colorAttachments[0].pixelFormat = pixelFormat 23 | 24 | try pipelineState = device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) 25 | } catch let error { 26 | print("Failed to create pipeline state, error \(error)") 27 | } 28 | 29 | } 30 | 31 | func encode(to commandBuffer: MTLCommandBuffer, 32 | pass: MTLRenderPassDescriptor, 33 | texture: MTLTexture) { 34 | 35 | let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: pass) 36 | renderEncoder?.label = "texture render" 37 | renderEncoder?.setRenderPipelineState(pipelineState) 38 | 39 | renderEncoder?.pushDebugGroup("texture") 40 | 41 | renderEncoder?.setFragmentTexture(texture, index: 0) 42 | renderEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) 43 | 44 | renderEncoder?.popDebugGroup() 45 | renderEncoder?.endEncoding() 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/vger/vgerScene.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #import 6 | #define VGER_MAX_LAYERS 4 7 | 8 | #import "prim.h" 9 | #import "paint.h" 10 | #import 11 | 12 | using namespace simd; 13 | 14 | template 15 | struct GPUVec { 16 | id buffer; 17 | T* ptr = nullptr; 18 | size_t count = 0; 19 | size_t capacity = 1024; 20 | static constexpr size_t MaxBufferSizeBytes = 1024 * 1024 * 256; 21 | 22 | GPUVec() { } 23 | 24 | GPUVec(id device) { 25 | buffer = [device newBufferWithLength:capacity * sizeof(T) 26 | options:MTLResourceStorageModeShared]; 27 | ptr = static_cast(buffer.contents); 28 | } 29 | 30 | void allocate(id device, size_t cap) { 31 | capacity = cap; 32 | auto oldBuffer = buffer; 33 | buffer = [device newBufferWithLength:capacity * sizeof(T) 34 | options:MTLResourceStorageModeShared]; 35 | memcpy(buffer.contents, oldBuffer.contents, count * sizeof(T)); 36 | ptr = static_cast(buffer.contents); 37 | } 38 | 39 | void append(const T& value) { 40 | 41 | if(count >= capacity && capacity*2*sizeof(T) <= MaxBufferSizeBytes) { 42 | allocate(buffer.device, capacity*2); 43 | } 44 | 45 | if(count < capacity) { 46 | ptr[count++] = value; 47 | } 48 | } 49 | 50 | void clear() { 51 | count = 0; 52 | } 53 | }; 54 | 55 | struct vgerScene { 56 | GPUVec prims[VGER_MAX_LAYERS]; 57 | GPUVec cvs; 58 | GPUVec xforms; 59 | GPUVec paints; 60 | 61 | void clear() { 62 | for(int layer=0;layer 4 | #import "../../Sources/vger/vgerPathScanner.h" 5 | 6 | @interface vgerPathScannerTests : XCTestCase 7 | 8 | @end 9 | 10 | @implementation vgerPathScannerTests 11 | 12 | - (void)setUp { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | - (void)tearDown { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | - (void)testScan { 21 | 22 | vgerPathScanner scan; 23 | 24 | // circle-ish 25 | vector_float2 cvs[] = { {1,0}, {1,1}, {0,1}, {-1, 1}, {-1,0}, {-1, -1}, {0,-1}, {1,-1}, {1,0}}; 26 | scan.begin(cvs, sizeof(cvs)/sizeof(vector_float2)); 27 | 28 | XCTAssertEqual(scan.segments.size(), 4); 29 | 30 | while(scan.next()) { 31 | printf("interval %f %f, active: ", scan.interval.a, scan.interval.b); 32 | for(int a = scan.first; a != -1; a = scan.segments[a].next) { 33 | printf("%d ", a); 34 | } 35 | printf("\n"); 36 | } 37 | 38 | printf("done\n"); 39 | } 40 | 41 | - (void)testScanCGPath { 42 | 43 | auto path = CGPathCreateMutable(); 44 | 45 | vector_float2 cvs[] = { {1,0}, {1,1}, {0,1}, {-1, 1}, {-1,0}, {-1, -1}, {0,-1}, {1,-1}, {1,0}}; 46 | 47 | int n = sizeof(cvs)/sizeof(vector_float2); 48 | CGPathMoveToPoint(path, nullptr, cvs[0].x, cvs[0].y); 49 | for(int i=1;i(0,1,1,1) 10 | let magenta = SIMD4(1,0,1,1) 11 | 12 | func textAt(_ vger: vgerContext, _ x: Float, _ y: Float, _ string: String) { 13 | vgerSave(vger) 14 | vgerTranslate(vger, .init(x: x, y: y)) 15 | vgerText(vger, string, cyan, 0) 16 | vgerRestore(vger) 17 | } 18 | 19 | func draw(vger: vgerContext, size: CGSize) { 20 | vgerSave(vger) 21 | 22 | let bezPaint = vgerLinearGradient(vger, .init(x: 50, y: 450), .init(x: 100, y: 450), cyan, magenta, 0.0) 23 | vgerStrokeBezier(vger, vgerBezierSegment(a: .init(x: 50, y: 450), b: .init(x: 100, y: 450), c: .init(x: 100, y: 500)), 1.0, bezPaint) 24 | textAt(vger, 150, 450, "Quadratic Bezier stroke") 25 | 26 | let rectPaint = vgerLinearGradient(vger, .init(x: 50, y: 350), .init(x: 100, y: 400), cyan, magenta, 0.0) 27 | vgerFillRect(vger, .init(x: 50, y: 350), .init(x: 100, y: 400), 10.0, rectPaint) 28 | textAt(vger, 150, 350, "Rounded rectangle") 29 | 30 | let circlePaint = vgerLinearGradient(vger, .init(x: 50, y: 250), .init(x: 100, y: 300), cyan, magenta, 0.0) 31 | vgerFillCircle(vger, .init(x: 75, y: 275), 25, circlePaint) 32 | textAt(vger, 150, 250, "Circle") 33 | 34 | let linePaint = vgerLinearGradient(vger, .init(x: 50, y: 150), .init(x: 100, y: 200), cyan, magenta, 0.0) 35 | vgerStrokeSegment(vger, .init(x: 50, y: 150), .init(x: 100, y: 200), 2.0, linePaint) 36 | textAt(vger, 150, 150, "Line segment") 37 | 38 | let theta: Float = 0.0 // orientation 39 | let aperture: Float = 0.5 * .pi 40 | 41 | let arcPaint = vgerLinearGradient(vger, .init(x: 50, y: 50), .init(x: 100, y: 100), cyan, magenta, 0.0) 42 | vgerStrokeArc(vger, .init(x: 75, y: 75), 25, 1.0, theta, aperture, arcPaint) 43 | textAt(vger, 150, 050, "Arc") 44 | 45 | vgerRestore(vger); 46 | } 47 | 48 | var body: some View { 49 | VgerView(renderCallback: draw) 50 | } 51 | } 52 | 53 | struct DemoView_Previews: PreviewProvider { 54 | static var previews: some View { 55 | DemoView() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/vgerTests/vgerTextureManagerTests.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import 4 | #import "../../Sources/vger/vgerTextureManager.h" 5 | #import 6 | #import "testUtils.h" 7 | 8 | @interface vgerTextureManagerTests : XCTestCase { 9 | id device; 10 | id queue; 11 | MTKTextureLoader* loader; 12 | } 13 | 14 | @end 15 | 16 | @implementation vgerTextureManagerTests 17 | 18 | - (void)setUp { 19 | device = MTLCreateSystemDefaultDevice(); 20 | loader = [[MTKTextureLoader alloc] initWithDevice: device]; 21 | queue = [device newCommandQueue]; 22 | } 23 | 24 | - (void)tearDown { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | } 27 | 28 | - (NSURL*) getImageURL:(NSString*)name { 29 | NSString* path = @"Contents/Resources/vger_vgerTests.bundle/Contents/Resources/images/"; 30 | path = [path stringByAppendingString:name]; 31 | NSBundle* bundle = [NSBundle bundleForClass:self.class]; 32 | return [bundle.bundleURL URLByAppendingPathComponent:path]; 33 | } 34 | 35 | - (id) getTexture:(NSString*)name { 36 | 37 | NSError* error; 38 | id tex = [loader newTextureWithContentsOfURL:[self getImageURL:name] options:nil error:&error]; 39 | assert(error == nil); 40 | return tex; 41 | } 42 | 43 | - (void)testPackTextures { 44 | vgerTextureManager* mgr = [[vgerTextureManager alloc] initWithDevice:device pixelFormat:MTLPixelFormatRGBA8Unorm]; 45 | 46 | id tex[5]; 47 | int sz = 16; 48 | for(int i=0;i<5;++i, sz*=2) { 49 | tex[i] = [self getTexture:[NSString stringWithFormat:@"icon-mac-%d.png", sz]]; 50 | } 51 | 52 | for(int i=0;i<200;++i) { 53 | [mgr addRegion:tex[rand() % 5]]; 54 | } 55 | 56 | id buf = [queue commandBuffer]; 57 | [mgr update:buf]; 58 | 59 | // Sync texture on macOS 60 | #if TARGET_OS_OSX 61 | auto blitEncoder = [buf blitCommandEncoder]; 62 | [blitEncoder synchronizeResource:mgr.atlas]; 63 | [blitEncoder endEncoding]; 64 | #endif 65 | 66 | [buf commit]; 67 | [buf waitUntilCompleted]; 68 | 69 | assert(buf.error == nil); 70 | 71 | showTexture(mgr.atlas, @"atlas.png"); 72 | } 73 | 74 | 75 | 76 | @end 77 | -------------------------------------------------------------------------------- /Demo/Shared/TigerView.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | import SwiftUI 4 | import vger 5 | import vgerSwift 6 | 7 | class TigerModel : ObservableObject { 8 | 9 | var image: UnsafeMutablePointer 10 | var scale = CGFloat(1.0) 11 | 12 | init() { 13 | let path = Bundle.main.path(forResource: "Ghostscript_Tiger", ofType: "svg") 14 | image = nsvgParseFromFile(path, "px", 96)! 15 | } 16 | 17 | func draw(vger: vgerContext, size: CGSize) { 18 | vgerSave(vger) 19 | 20 | vgerTranslate(vger, .init(0, Float(size.height))) 21 | vgerScale(vger, .init(0.5, -0.5)) 22 | vgerScale(vger, .init(repeating: Float(scale))) 23 | 24 | var shape = image.pointee.shapes 25 | while let s = shape { 26 | 27 | let c = s.pointee.fill.color 28 | 29 | let fcolor = 1.0/255.0 * SIMD4( Float(c & 0xff), Float( (c>>8) & 0xff), 30 | Float( (c>>16) & 0xff), Float( (c>>24) & 0xff )) 31 | 32 | let paint = vgerColorPaint(vger, fcolor) 33 | 34 | var path = s.pointee.paths 35 | while let p = path { 36 | let n = Int(p.pointee.npts) 37 | p.pointee.pts.withMemoryRebound(to: SIMD2.self, capacity:n) { cvs in 38 | vgerMoveTo(vger, cvs[0]) 39 | var i = 1 40 | while i < n-2 { 41 | vgerCubicApproxTo(vger, cvs[i], cvs[i+1], cvs[i+2]) 42 | i += 3 43 | } 44 | } 45 | path = p.pointee.next 46 | } 47 | vgerFill(vger, paint) 48 | 49 | shape = s.pointee.next 50 | } 51 | 52 | vgerRestore(vger) 53 | } 54 | } 55 | 56 | struct TigerView: View { 57 | 58 | var model: TigerModel 59 | 60 | var body: some View { 61 | GeometryReader { geom in 62 | VgerView { vger, _ in 63 | model.draw(vger: vger, size: geom.size) 64 | } 65 | .gesture(MagnificationGesture().onChanged({ scale in 66 | model.scale = scale 67 | })) 68 | } 69 | 70 | } 71 | } 72 | 73 | struct TigerView_Previews: PreviewProvider { 74 | static var previews: some View { 75 | TigerView(model: TigerModel()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/vgerSwift/VgerView.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | import SwiftUI 4 | import MetalKit 5 | import vger 6 | 7 | #if os(macOS) 8 | 9 | public struct VgerView: NSViewRepresentable { 10 | 11 | var renderCallback : (vgerContext, CGSize) -> Void 12 | 13 | public init(renderCallback: @escaping (vgerContext, CGSize) -> Void) { 14 | self.renderCallback = renderCallback 15 | } 16 | 17 | public class Coordinator { 18 | var renderer = Renderer(device: MTLCreateSystemDefaultDevice()!) 19 | } 20 | 21 | public func makeCoordinator() -> Coordinator { 22 | return Coordinator() 23 | } 24 | 25 | public func makeNSView(context: Context) -> MTKView { 26 | let metalView = MTKView(frame: CGRect(x: 0, y: 0, width: 1024, height: 768), 27 | device: MTLCreateSystemDefaultDevice()!) 28 | metalView.clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0) 29 | metalView.delegate = context.coordinator.renderer 30 | metalView.isPaused = true 31 | metalView.enableSetNeedsDisplay = true 32 | context.coordinator.renderer.renderCallback = renderCallback 33 | return metalView 34 | } 35 | 36 | public func updateNSView(_ nsView: MTKView, context: Context) { 37 | context.coordinator.renderer.renderCallback = renderCallback 38 | nsView.setNeedsDisplay() 39 | } 40 | } 41 | 42 | #else 43 | 44 | public struct VgerView: UIViewRepresentable { 45 | 46 | var renderCallback : (vgerContext, CGSize) -> Void 47 | 48 | public init(renderCallback: @escaping (vgerContext, CGSize) -> Void) { 49 | self.renderCallback = renderCallback 50 | } 51 | 52 | public class Coordinator { 53 | var renderer = Renderer(device: MTLCreateSystemDefaultDevice()!) 54 | } 55 | 56 | public func makeCoordinator() -> Coordinator { 57 | return Coordinator() 58 | } 59 | 60 | public func makeUIView(context: Context) -> MTKView { 61 | let metalView = MTKView(frame: CGRect(x: 0, y: 0, width: 1024, height: 768), 62 | device: MTLCreateSystemDefaultDevice()!) 63 | metalView.clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0) 64 | metalView.delegate = context.coordinator.renderer 65 | metalView.isPaused = true 66 | metalView.enableSetNeedsDisplay = true 67 | context.coordinator.renderer.renderCallback = renderCallback 68 | return metalView 69 | } 70 | 71 | public func updateUIView(_ uiView: MTKView, context: Context) { 72 | context.coordinator.renderer.renderCallback = renderCallback 73 | uiView.setNeedsDisplay() 74 | } 75 | } 76 | 77 | #endif 78 | 79 | struct VgerView_Previews: PreviewProvider { 80 | static var previews: some View { 81 | VgerView(renderCallback: { vger, _ in 82 | vgerText(vger, "hello world", .init(repeating: 1), 0) 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | }, 93 | { 94 | "idiom" : "mac", 95 | "scale" : "1x", 96 | "size" : "16x16" 97 | }, 98 | { 99 | "idiom" : "mac", 100 | "scale" : "2x", 101 | "size" : "16x16" 102 | }, 103 | { 104 | "idiom" : "mac", 105 | "scale" : "1x", 106 | "size" : "32x32" 107 | }, 108 | { 109 | "idiom" : "mac", 110 | "scale" : "2x", 111 | "size" : "32x32" 112 | }, 113 | { 114 | "idiom" : "mac", 115 | "scale" : "1x", 116 | "size" : "128x128" 117 | }, 118 | { 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "128x128" 122 | }, 123 | { 124 | "idiom" : "mac", 125 | "scale" : "1x", 126 | "size" : "256x256" 127 | }, 128 | { 129 | "idiom" : "mac", 130 | "scale" : "2x", 131 | "size" : "256x256" 132 | }, 133 | { 134 | "idiom" : "mac", 135 | "scale" : "1x", 136 | "size" : "512x512" 137 | }, 138 | { 139 | "idiom" : "mac", 140 | "scale" : "2x", 141 | "size" : "512x512" 142 | } 143 | ], 144 | "info" : { 145 | "author" : "xcode", 146 | "version" : 1 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/vger/vgerGlyphPathCache.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import "vgerGlyphPathCache.h" 4 | #import "sdf.h" 5 | 6 | using namespace simd; 7 | 8 | vgerGlyphPathCache::vgerGlyphPathCache() { 9 | 10 | auto bundle = SWIFTPM_MODULE_BUNDLE; 11 | assert(bundle); 12 | auto fontURL = [bundle URLForResource:@"Anodina-Regular" withExtension:@"ttf" subdirectory:@"fonts"]; 13 | 14 | auto fd = CTFontManagerCreateFontDescriptorsFromURL( (__bridge CFURLRef) fontURL); 15 | ctFont = CTFontCreateWithFontDescriptor( (CTFontDescriptorRef) CFArrayGetValueAtIndex(fd, 0), 12.0, nil); 16 | assert(ctFont); 17 | CFRelease(fd); 18 | 19 | } 20 | 21 | vgerGlyphPathCache::~vgerGlyphPathCache() { 22 | CFRelease(ctFont); 23 | } 24 | 25 | static bool scanGlyphs = true; 26 | 27 | vgerGlyphPathCache::Info& vgerGlyphPathCache::getInfo(CGGlyph glyph) { 28 | 29 | auto iter = _cache.find(glyph); 30 | if(iter != _cache.end()) { 31 | return iter->second; 32 | } 33 | 34 | // Add a glyph to the cache. 35 | CGRect boundingRect; 36 | CTFontGetBoundingRectsForGlyphs(ctFont, kCTFontOrientationHorizontal, &glyph, &boundingRect, 1); 37 | 38 | auto glyphTransform = CGAffineTransformMake(1, 0, 0, 1, 39 | -boundingRect.origin.x, 40 | -boundingRect.origin.y); 41 | auto path = CTFontCreatePathForGlyph(ctFont, glyph, &glyphTransform); 42 | 43 | auto& info = _cache[glyph]; 44 | scan.begin(path); 45 | 46 | if(scanGlyphs) { 47 | 48 | while(scan.next()) { 49 | int n = scan.activeCount; 50 | 51 | vgerPrim prim = { 52 | .type = vgerPathFill, 53 | .start = (uint32_t) info.cvs.size(), 54 | .count = uint16_t(n) 55 | }; 56 | 57 | Interval xInt{FLT_MAX, -FLT_MAX}; 58 | 59 | for(int a = scan.first; a != -1; a = scan.segments[a].next) { 60 | 61 | assert(a < scan.segments.size()); 62 | for(int i=0;i<3;++i) { 63 | auto p = scan.segments[a].cvs[i]; 64 | info.cvs.push_back(p); 65 | xInt.a = std::min(xInt.a, p.x); 66 | xInt.b = std::max(xInt.b, p.x); 67 | } 68 | 69 | } 70 | 71 | BBox bounds; 72 | bounds.min.x = xInt.a; 73 | bounds.max.x = xInt.b; 74 | bounds.min.y = scan.interval.a; 75 | bounds.max.y = scan.interval.b; 76 | 77 | // Calculate the prim vertices at this stage, 78 | // as we do for glyphs. 79 | prim.quadBounds[0] = prim.texBounds[0] = bounds.min; 80 | prim.quadBounds[1] = prim.texBounds[1] = bounds.max; 81 | 82 | info.prims.push_back(prim); 83 | } 84 | 85 | } else { 86 | 87 | vgerPrim prim = { 88 | .type = vgerPathFill, 89 | .start = (uint32_t) info.cvs.size(), 90 | .count = (uint16_t) scan.segments.size() 91 | }; 92 | 93 | for(auto& seg : scan.segments) { 94 | for(int i=0;i<3;++i) { 95 | info.cvs.push_back(seg.cvs[i]); 96 | } 97 | } 98 | 99 | BBox bounds = sdPrimBounds(prim, info.cvs.data()); 100 | 101 | // Calculate the prim vertices at this stage, 102 | // as we do for glyphs. 103 | prim.quadBounds[0] = prim.texBounds[0] = bounds.min; 104 | prim.quadBounds[1] = prim.texBounds[1] = bounds.max; 105 | 106 | info.prims.push_back(prim); 107 | } 108 | 109 | CGPathRelease(path); 110 | 111 | return info; 112 | } 113 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | vger is a GPU-accelerated vector graphics renderer written in C with Swift bindings, designed for iOS and macOS. It renders primitives analytically in fragment shaders rather than using CPU tessellation, achieving high performance for immediate-mode UIs. 8 | 9 | ## Architecture 10 | 11 | ### Core Components 12 | 13 | - **C Core (`Sources/vger/`)**: The main rendering engine written in C/C++/Objective-C++ with Metal shaders 14 | - `vger.h`: Main API interface - review this for all available rendering functions 15 | - `vger.mm`: Core implementation with Metal rendering logic 16 | - `vgerRenderer.mm`: Main renderer class handling Metal command encoding 17 | - `vgerPathScanner.mm`: Path processing and horizontal slab decomposition for fills 18 | - `vgerGlyphCache.mm`: Text rendering with glyph atlas management 19 | - `vger.metal`: Fragment shaders for primitive rendering 20 | 21 | - **Swift Layer (`Sources/vgerSwift/`)**: SwiftUI integration and higher-level APIs 22 | - `VgerView.swift`: SwiftUI view wrapper for Metal rendering 23 | - `Renderer.swift`: Metal rendering coordinator 24 | - `TextureRenderer.swift`: Additional texture rendering utilities 25 | 26 | - **Demo App (`Demo/`)**: Complete iOS/macOS SwiftUI application demonstrating usage 27 | - Cross-platform implementation showing VgerView integration 28 | - Examples of text, shapes, SVG rendering, and various primitives 29 | 30 | ### Key Design Patterns 31 | 32 | - **Immediate Mode**: All drawing commands are recorded per frame, no retained geometry 33 | - **GPU-Centric**: Fragment shaders handle primitive evaluation, minimal CPU work 34 | - **Instanced Rendering**: Each primitive renders as a quad with shader-computed geometry 35 | - **Paint System**: Separate paint objects for colors, gradients, textures, and patterns 36 | - **Transform Stack**: Standard graphics transform state with save/restore 37 | 38 | ## Development Commands 39 | 40 | ### Building 41 | ```bash 42 | # Build the project 43 | ./build.sh 44 | 45 | # Or use xcodebuild directly 46 | xcodebuild -scheme vger -sdk macosx -destination "name=My Mac" 47 | ``` 48 | 49 | ### Testing 50 | ```bash 51 | # Run all tests 52 | ./test.sh 53 | 54 | # Or use xcodebuild directly 55 | xcodebuild test -scheme vger -sdk macosx -destination "name=My Mac" 56 | ``` 57 | 58 | ### Swift Package Manager 59 | This project uses SPM and can be built with: 60 | ```bash 61 | swift build 62 | swift test 63 | ``` 64 | 65 | ## Usage Patterns 66 | 67 | ### Basic C API Usage 68 | ```c 69 | vgerContext vg = vgerNew(0, MTLPixelFormatBGRA8Unorm); 70 | vgerBegin(vg, width, height, devicePixelRatio); 71 | 72 | // Create paint and draw primitives 73 | vgerPaintIndex paint = vgerColorPaint(vg, (vector_float4){1,0,0,1}); 74 | vgerFillCircle(vg, center, radius, paint); 75 | 76 | vgerEncode(vg, commandBuffer, renderPassDescriptor); 77 | ``` 78 | 79 | ### SwiftUI Integration 80 | ```swift 81 | VgerView(renderCallback: { vger, size in 82 | let paint = vgerColorPaint(vger, SIMD4(1,0,1,1)) 83 | vgerFillRect(vger, min, max, cornerRadius, paint) 84 | }) 85 | ``` 86 | 87 | ## Key Implementation Details 88 | 89 | - **Path Fills**: Uses reverse Loop-Blinn technique in fragment shader to avoid solving quadratic equations 90 | - **Text Rendering**: Glyph atlas with SDF-based rendering for scalable text 91 | - **Buffering**: Supports double/triple buffering schemes via creation flags 92 | - **Layers**: Multi-layer rendering support (up to 4 layers) 93 | - **Transform Stack**: Full 2D transformation support with save/restore 94 | 95 | ## Test Structure 96 | 97 | Tests are in `Tests/vgerTests/` and include: 98 | - Basic primitive rendering tests with reference images 99 | - Path scanning and fill algorithm tests 100 | - Glyph cache and text rendering tests 101 | - SDF computation tests 102 | - Texture management tests 103 | 104 | Reference images in `Tests/vgerTests/images/` are used for visual regression testing. -------------------------------------------------------------------------------- /Tests/vgerTests/sdfTests.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import 4 | #import 5 | #include "vger.h" 6 | 7 | #import "../../Sources/vger/sdf.h" 8 | 9 | @interface sdfTests : XCTestCase 10 | 11 | @end 12 | 13 | @implementation sdfTests 14 | 15 | - (void) testWire { 16 | 17 | float d = sdWire(float2{0,0}, float2{0,0}, float2{1,1}); 18 | XCTAssertEqualWithAccuracy(d, 0, 0.1); 19 | 20 | d = sdWire(float2{1,1}, float2{0,0}, float2{1,1}); 21 | XCTAssertEqualWithAccuracy(d, 0, 0.1); 22 | 23 | d = sdWire(float2{0.5,0.5}, float2{0,0}, float2{1,1}); 24 | XCTAssertEqualWithAccuracy(d, 0, 0.1); 25 | 26 | } 27 | 28 | - (void) testLineTest { 29 | 30 | float2 i{1,0}; 31 | float2 j{0,1}; 32 | 33 | XCTAssertTrue(lineTest(0, i-j, i+j)); 34 | XCTAssertFalse(lineTest(2*j, i-j, i+j)); 35 | 36 | XCTAssertFalse(lineTest( float2{13.5, 13.5}, float2{60.0, 60.0}, float2{35.0, 35.0} )); // XXX: GPU seems to return true 37 | XCTAssertFalse(lineTest( float2{36.5, 36.5}, float2{60.0, 60.0}, float2{35.0, 35.0} )); 38 | XCTAssertTrue(lineTest( float2{36.4999961853, 36.5}, float2{60.0, 60.0}, float2{35.0, 35.0} )); 39 | 40 | } 41 | 42 | static void printBezierTest(float2 A, float2 B, float2 C) { 43 | for(float y=2; y>=0; y-=.1) { 44 | for(float x=0; x<2; x+=.1) { 45 | putchar( bezierTest(float2{x,y}, A, B, C) ? '*' : ' '); 46 | } 47 | putchar('\n'); 48 | } 49 | } 50 | 51 | - (void) testBezierTest { 52 | 53 | float2 i{1,0}; 54 | float2 j{0,1}; 55 | 56 | XCTAssertEqual(bezierTest(i+.1*j, 0, i+j, 2*i), 1); 57 | XCTAssertEqual(bezierTest(i+.9*j, 0, i+j, 2*i), 0); 58 | XCTAssertEqual(bezierTest(i+2*j, 0, i+j, 2*i), 0); 59 | 60 | XCTAssertEqual(bezierTest(i-.9*j, 0, i-j, 2*i), 0); 61 | 62 | 63 | printBezierTest(0, i+j, 2*i); 64 | 65 | printBezierTest(0, i+2*j, 2*i); 66 | 67 | printBezierTest(j, i, 2*i+j); 68 | 69 | printBezierTest(2*j, i, 2*i+2*j); 70 | 71 | 72 | } 73 | 74 | - (void) testSdLine { 75 | 76 | float2 a{0,0}; 77 | float2 b{1,0}; 78 | float2 c{2,0}; 79 | 80 | auto d = sdLine(float2{1,1}, a, b); 81 | XCTAssertEqualWithAccuracy(d, 1.0, 0.001); 82 | 83 | d = sdLine(float2{1,-1}, a, b); 84 | XCTAssertEqualWithAccuracy(d, -1.0, 0.001); 85 | 86 | d = sdLine(float2{1,1}, a, c); 87 | XCTAssertEqualWithAccuracy(d, 1.0, 0.001); 88 | } 89 | 90 | - (void) testBezierCollinear { 91 | 92 | float2 a{0,0}; 93 | float2 b{1,0}; 94 | float2 c{2,0}; 95 | 96 | auto d = udBezier(float2{1,1}, a, b, c); 97 | XCTAssertEqualWithAccuracy(d, 1.0, 0.001); 98 | 99 | d = udBezier(float2{1,-1}, a, b, c); 100 | XCTAssertEqualWithAccuracy(d, 1.0, 0.001); 101 | } 102 | 103 | static void printSdBezier(float2 A, float2 B, float2 C) { 104 | for(float y=1; y>=-1; y-=.1) { 105 | for(float x=-1; x<3; x+=.1) { 106 | putchar( sdBezier(float2{x,y}, A, B, C) < 0 ? '*' : ' '); 107 | } 108 | putchar('\n'); 109 | } 110 | } 111 | 112 | static void checkSdBezier(float2 A, float2 B, float2 C) { 113 | for(float y=1; y>=-1; y-=.1) { 114 | for(float x=-1; x<3; x+=.1) { 115 | float2 xy{x, y}; 116 | XCTAssertEqualWithAccuracy( 117 | abs(sdBezier(xy, A, B, C)), 118 | udBezier(xy, A, B, C), 119 | 0.001); 120 | } 121 | } 122 | } 123 | 124 | - (void) testSdBezier { 125 | 126 | float2 a{0,0}; 127 | float2 b{1,1}; 128 | float2 c{2,0}; 129 | 130 | XCTAssertEqualWithAccuracy(sdBezier(float2{0,0}, a, b, c), 0.0, 0.001); 131 | XCTAssertEqualWithAccuracy(sdBezier(float2{1,0}, a, b, c), 0.5, 0.001); 132 | XCTAssertEqualWithAccuracy(sdBezier(float2{1,1}, a, b, c), -0.5, 0.001); 133 | 134 | printf("\n\n"); 135 | printSdBezier(a, b, c); 136 | 137 | printf("\n\n"); 138 | printSdBezier(c, b, a); 139 | 140 | printf("\n\n"); 141 | printSdBezier(a, float2{1, -1}, c); 142 | 143 | checkSdBezier(a, b, c); 144 | checkSdBezier(c, b, a); 145 | checkSdBezier(a, float2{1, -1}, c); 146 | } 147 | 148 | @end 149 | -------------------------------------------------------------------------------- /Sources/vger/vgerTextureManager.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import "vgerTextureManager.h" 4 | #include "stb_rect_pack.h" 5 | #include 6 | 7 | #define ATLAS_SIZE 2048 8 | 9 | @interface vgerTextureManager() { 10 | id _device; 11 | MTLTextureDescriptor* atlasDesc; 12 | std::vector nodes; 13 | std::vector regions; 14 | stbrp_context ctx; 15 | NSMutableArray< id >* newTextures; 16 | size_t areaUsed; 17 | } 18 | @end 19 | 20 | @implementation vgerTextureManager 21 | 22 | - (instancetype) initWithDevice:(id)device pixelFormat:(MTLPixelFormat)format { 23 | self = [super init]; 24 | if (self) { 25 | _device = device; 26 | atlasDesc = [MTLTextureDescriptor 27 | texture2DDescriptorWithPixelFormat:format 28 | width:ATLAS_SIZE 29 | height:ATLAS_SIZE 30 | mipmapped:NO]; 31 | nodes.resize(2*ATLAS_SIZE); 32 | stbrp_init_target(&ctx, ATLAS_SIZE, ATLAS_SIZE, nodes.data(), 2*ATLAS_SIZE); 33 | 34 | atlasDesc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; 35 | #if TARGET_OS_OSX || TARGET_OS_MACCATALYST 36 | atlasDesc.storageMode = MTLStorageModeManaged; 37 | #else 38 | atlasDesc.storageMode = MTLStorageModeShared; 39 | #endif 40 | 41 | _atlas = [device newTextureWithDescriptor:atlasDesc]; 42 | assert(self.atlas); 43 | 44 | newTextures = [NSMutableArray new]; 45 | } 46 | return self; 47 | } 48 | 49 | - (int) addRegion:(const uint8_t *)data width:(int)width height:(int)height bytesPerRow:(NSUInteger)bytesPerRow { 50 | 51 | auto desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:atlasDesc.pixelFormat width:width height:height mipmapped:NO]; 52 | desc.usage = MTLTextureUsageShaderRead; 53 | #if TARGET_OS_OSX || TARGET_OS_MACCATALYST 54 | desc.storageMode = MTLStorageModeManaged; 55 | #else 56 | desc.storageMode = MTLStorageModeShared; 57 | #endif 58 | auto tex = [_device newTextureWithDescriptor:desc]; 59 | assert(tex); 60 | 61 | [tex replaceRegion:MTLRegionMake2D(0, 0, width, height) mipmapLevel:0 withBytes:data bytesPerRow:bytesPerRow]; 62 | return [self addRegion:tex]; 63 | 64 | } 65 | 66 | /// Add region for an already loaded texture. 67 | - (int) addRegion:(id)texture { 68 | 69 | [newTextures addObject:texture]; 70 | return int(regions.size() + newTextures.count); 71 | 72 | } 73 | 74 | // Add any new textures and repack if necessary. 75 | - (void) update:(id) buffer { 76 | 77 | if(newTextures.count) { 78 | 79 | // Pack new regions. 80 | std::vector newRegions; 81 | for(id tex in newTextures) { 82 | stbrp_rect r; 83 | r.w = tex.width; 84 | r.h = tex.height; 85 | newRegions.push_back(r); 86 | areaUsed += r.w*r.h; 87 | } 88 | 89 | if(stbrp_pack_rects(&ctx, newRegions.data(), int(newRegions.size()))) { 90 | 91 | auto e = [buffer blitCommandEncoder]; 92 | for(int i=0;i 6 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Faudulus%2Fvger%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/audulus/vger) 7 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Faudulus%2Fvger%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/audulus/vger) 8 | 9 | vger is a vector graphics renderer which renders a limited set of primitives, but does so almost entirely on the GPU. Works on iOS and macOS. API is plain C. Used to render [Audulus](https://audulus.com). Rust port is [here](https://github.com/audulus/vger-rs). 10 | 11 | demo 12 | 13 | Each primitive can be filled with a solid color, gradient, or texture. vger renders primitives as instanced quads, with most of the calculations done in the fragment shader. 14 | 15 | Here's a screenshot of vger rendering the Audulus UI: 16 | 17 | 18 | 19 | Here's it rendering that svg tiger (the cubic curves are converted to quadratic by a lousy method, and I've omitted the strokes): 20 | 21 | 22 | 23 | ## Why? 24 | 25 | I was previously using nanovg for Audulus, which was consuming too much CPU for the immediate-mode UI. nanovg is certainly more full featured, but for Audulus, vger maintains 120fps while nanovg falls to 30fps on my 120Hz iPad because of CPU-side path tessellation, and other overhead. vger renders analytically without tessellation, leaning heavily on the fragment shader. 26 | 27 | If you're interested in non-apple platforms, check out [the rust port](https://github.com/audulus/vger-rs). 28 | 29 | ## How it works 30 | 31 | vger draws a quad for each primitive and computes the actual primitive shape in the fragment function. For path fills, vger splits paths into horizontal slabs (see `vgerPathScanner`) to reduce the number of tests in the fragment function. 32 | 33 | The bezier path fill case is somewhat original. To avoid having to solve quadratic equations (which has numerical issues), the fragment function uses a sort-of reverse Loop-Blinn. To determine if a point is inside or outside, vger tests against the lines formed between the endpoints of each bezier curve, flipping inside/outside for each intersection with a +x ray from the point. Then vger tests the point against the area between the bezier segment and the line, flipping inside/outside again if inside. This avoids the pre-computation of [Loop-Blinn](https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p1000-loop.pdf), and the AA issues of [Kokojima](https://dl.acm.org/doi/10.1145/1179849.1179997). 34 | 35 | ## Status 36 | 37 | - ✅ Quadratic bezier strokes 38 | - ✅ Round Rectangles 39 | - ✅ Circles 40 | - ✅ Line segments (need square ends for Audulus) 41 | - ✅ Arcs 42 | - ✅ Text (Audulus only uses one font, but could add support for more if anyone is interested) 43 | - ✅ Multi-line text 44 | - ✅ Path Fills. 45 | 46 | ## Installation 47 | 48 | To add vger to your Xcode project, select File -> Swift Packages -> Add Package Depedancy. Enter https://github.com/audulus/vger for the URL. Check the use branch option and enter `main`. 49 | 50 | ## Usage 51 | 52 | See [`vger.h`](https://github.com/audulus/vger/blob/main/Sources/vger/include/vger.h) for the complete API. You can get a good sense of the usage by looking at [these tests](https://github.com/audulus/vger/blob/main/Tests/vgerTests/vgerTests.mm). 53 | 54 | Vger has a C interface and can be used from C, C++, ObjC, or Swift. `vgerEncode` must be called from either ObjC or Swift since it takes a `MTLCommandBuffer`. 55 | 56 | See [the demo app](https://github.com/audulus/vger/blob/main/Demo) for an example of using vger in a iOS/macOS SwiftUI app. vger includes `VgerView` to make it really easy to use Vger within SwiftUI: 57 | 58 | ```swift 59 | import SwiftUI 60 | import vger // C/C++/ObjC interface. 61 | import vgerSwift // Swift nicities. 62 | 63 | struct HelloView: View { 64 | 65 | let cyan = SIMD4(0,1,1,1) 66 | 67 | var body: some View { 68 | VgerView(renderCallback: { vger in 69 | vgerText(vger, "Hello world. This is V'Ger.", cyan, 0) 70 | }) 71 | } 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /Sources/vger/vgerPathScanner.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #include "vgerPathScanner.h" 4 | #include 5 | #include 6 | 7 | using namespace simd; 8 | 9 | bool operator<(const vgerPathScanner::Node& a, const vgerPathScanner::Node& b) { 10 | return std::tie(a.coord, a.seg, a.end) < std::tie(b.coord, b.seg, b.end); 11 | } 12 | 13 | void vgerPathScanner::_init() { 14 | 15 | nodes.clear(); 16 | index = 0; 17 | 18 | for(int i=0;ip; 57 | float2& start = scan->start; 58 | 59 | switch(element->type) { 60 | case kCGPathElementMoveToPoint: 61 | p = start = tof2(element->points[0]); 62 | break; 63 | 64 | case kCGPathElementAddLineToPoint: { 65 | float2 b = tof2(element->points[0]); 66 | scan->segments.push_back({p, (p+b)/2, b}); 67 | p = b; 68 | } 69 | break; 70 | 71 | case kCGPathElementAddQuadCurveToPoint: 72 | scan->segments.push_back({ 73 | p, tof2(element->points[0]), tof2(element->points[1]) 74 | }); 75 | 76 | p = tof2(element->points[1]); 77 | break; 78 | 79 | case kCGPathElementAddCurveToPoint: 80 | assert(false); // can't handle cubic curves yet. 81 | break; 82 | 83 | case kCGPathElementCloseSubpath: 84 | if(!equal(p, start)) { 85 | scan->segments.push_back({p, (p+start)/2, start}); 86 | } 87 | p = start; 88 | break; 89 | 90 | default: 91 | break; 92 | } 93 | 94 | } 95 | 96 | void vgerPathScanner::begin(CGPathRef path) { 97 | 98 | segments.clear(); 99 | p = float2{0,0}; 100 | start = float2{0,0}; 101 | 102 | CGPathApply(path, this, pathElement); 103 | 104 | _init(); 105 | 106 | } 107 | 108 | bool vgerPathScanner::next() { 109 | 110 | float y = nodes[index].coord; 111 | interval.a = y; 112 | auto n = nodes.size(); 113 | 114 | // Activate and deactivate segments. 115 | for(;index < n && nodes[index].coord == y; ++index) { 116 | assert(index < n); 117 | auto& node = nodes[index]; 118 | assert(node.seg < segments.size()); 119 | if(node.end) { 120 | --activeCount; 121 | auto& prev = segments[node.seg].previous; 122 | auto& next = segments[node.seg].next; 123 | if(prev != -1) { 124 | assert(prev < segments.size()); 125 | segments[prev].next = next; 126 | } 127 | if(next != -1) { 128 | assert(next < segments.size()); 129 | segments[next].previous = prev; 130 | } 131 | if(first == node.seg) { 132 | first = next; 133 | } 134 | next = prev = -1; 135 | } else { 136 | ++activeCount; 137 | segments[node.seg].next = first; 138 | if(first != -1) { 139 | assert(first < segments.size()); 140 | segments[first].previous = node.seg; 141 | } 142 | first = node.seg; 143 | } 144 | } 145 | 146 | if(index < n) { 147 | interval.b = nodes[index].coord; 148 | } 149 | 150 | return index < n; 151 | } 152 | -------------------------------------------------------------------------------- /Sources/vger/vgerGlyphCache.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import "vgerGlyphCache.h" 4 | #import "vgerTextureManager.h" 5 | #include 6 | 7 | @interface vgerGlyphCache() { 8 | vgerTextureManager* mgr; 9 | std::vector< std::vector > glyphs; 10 | CTFontRef ctFont; 11 | } 12 | @end 13 | 14 | @implementation vgerGlyphCache 15 | 16 | - (instancetype) initWithDevice:(id)device { 17 | self = [super init]; 18 | if (self) { 19 | mgr = [[vgerTextureManager alloc] initWithDevice:device pixelFormat:MTLPixelFormatA8Unorm]; 20 | 21 | auto bundle = SWIFTPM_MODULE_BUNDLE; 22 | assert(bundle); 23 | auto fontURL = [bundle URLForResource:@"Anodina-Regular" withExtension:@"ttf" subdirectory:@"fonts"]; 24 | 25 | auto fd = CTFontManagerCreateFontDescriptorsFromURL( (__bridge CFURLRef) fontURL); 26 | ctFont = CTFontCreateWithFontDescriptor( (CTFontDescriptorRef) CFArrayGetValueAtIndex(fd, 0), 12.0, nil); 27 | assert(ctFont); 28 | CFRelease(fd); 29 | //ctFont = CTFontCreateWithName((__bridge CFStringRef)@"Avenir-light", /*fontPointSize*/24, NULL); 30 | } 31 | return self; 32 | } 33 | 34 | - (void) dealloc { 35 | CFRelease(ctFont); 36 | } 37 | 38 | - (GlyphInfo) getGlyph:(CGGlyph)glyph scale:(float) scale { 39 | 40 | if(glyph >= glyphs.size()) { 41 | glyphs.resize(glyph+1); 42 | } 43 | 44 | auto& v = glyphs[glyph]; 45 | 46 | for(auto& info : v) { 47 | if(info.size == scale) { 48 | return info; 49 | } 50 | } 51 | 52 | // Render the glyph with CoreText. 53 | CGRect boundingRect; 54 | CTFontGetBoundingRectsForGlyphs(ctFont, kCTFontOrientationHorizontal, &glyph, &boundingRect, 1); 55 | 56 | auto glyphTransform = CGAffineTransformMake(1, 0, 0, 1, 57 | -boundingRect.origin.x, 58 | -boundingRect.origin.y); 59 | auto path = CTFontCreatePathForGlyph(ctFont, glyph, &glyphTransform); 60 | 61 | if(path == 0) { 62 | //NSLog(@"no path for glyph index %d\n", (int)glyph); 63 | return GlyphInfo(); 64 | } 65 | 66 | boundingRect.size.width *= scale; 67 | boundingRect.size.height *= scale; 68 | 69 | int width = ceilf(boundingRect.size.width) + 2*GLYPH_MARGIN; 70 | int height = ceilf(boundingRect.size.height) + 2*GLYPH_MARGIN; 71 | 72 | //NSLog(@"glyph size: %d %d\n", width, height); 73 | 74 | std::vector imageData(width*height); 75 | 76 | auto colorSpace = CGColorSpaceCreateDeviceGray(); 77 | auto bitmapInfo = (kCGBitmapAlphaInfoMask & kCGImageAlphaNone); 78 | auto context = CGBitmapContextCreate(imageData.data(), 79 | width, 80 | height, 81 | 8, 82 | width, 83 | colorSpace, 84 | bitmapInfo); 85 | 86 | CGContextTranslateCTM(context, GLYPH_MARGIN, GLYPH_MARGIN); 87 | CGContextScaleCTM(context, scale, scale); 88 | 89 | // Fill the context with an opaque black color 90 | CGContextSetRGBFillColor(context, 0, 0, 0, 1); 91 | CGContextFillRect(context, CGRectMake(0, 0, width, height)); 92 | 93 | // Set fill color so that glyphs are solid white 94 | CGContextSetRGBFillColor(context, 1, 1, 1, 1); 95 | 96 | CGContextAddPath(context, path); 97 | CGContextFillPath(context); 98 | 99 | //CGPoint p = {-boundingRect.origin.x, -boundingRect.origin.y}; 100 | //CTFontDrawGlyphs(ctFont, &glyph, &p, 1, context); 101 | 102 | auto region = [mgr addRegion:imageData.data() width:width height:height bytesPerRow:width]; 103 | 104 | GlyphInfo info = { 105 | .size=scale, 106 | .regionIndex=region, 107 | .textureWidth=width, 108 | .textureHeight=height, 109 | .glyphBounds=boundingRect 110 | }; 111 | 112 | v.push_back(info); 113 | 114 | CGPathRelease(path); 115 | CGContextRelease(context); 116 | CGColorSpaceRelease(colorSpace); 117 | 118 | return info; 119 | 120 | } 121 | 122 | - (void) update:(id) buffer { 123 | [mgr update:buffer]; 124 | } 125 | 126 | - (id) getAltas { 127 | return mgr.atlas; 128 | } 129 | 130 | - (stbrp_rect*) getRects { 131 | return [mgr getRects]; 132 | } 133 | 134 | - (CTFontRef) getFont { 135 | return ctFont; 136 | } 137 | 138 | - (float) usage { 139 | return mgr.usage; 140 | } 141 | 142 | @end 143 | -------------------------------------------------------------------------------- /Tests/vgerTests/testUtils.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #include "testUtils.h" 4 | #include 5 | #import 6 | #import 7 | 8 | void writeCGImage(CGImageRef image, CFURLRef url) { 9 | auto dest = CGImageDestinationCreateWithURL(url, (CFStringRef) UTTypePNG.identifier, 1, nil); 10 | CGImageDestinationAddImage(dest, image, nil); 11 | assert(CGImageDestinationFinalize(dest)); 12 | CFRelease(dest); 13 | } 14 | 15 | void releaseImageData(void * __nullable info, 16 | const void * data, size_t size) { 17 | free((void*)data); 18 | } 19 | 20 | CGImageRef createImage(UInt8* data, NSUInteger w, NSUInteger h) { 21 | 22 | UInt8* newData = (UInt8*) malloc(4*w*h); 23 | memcpy(newData, data, 4*w*h); 24 | 25 | auto provider = CGDataProviderCreateWithData(nullptr, 26 | newData, 27 | 4*w*h, 28 | releaseImageData); 29 | 30 | auto colorSpace = CGColorSpaceCreateDeviceRGB(); 31 | 32 | auto image = CGImageCreate(w, h, 33 | 8, 32, 34 | w*4, 35 | colorSpace, 36 | kCGImageAlphaNoneSkipLast, 37 | provider, 38 | nil, 39 | true, 40 | kCGRenderingIntentDefault); 41 | 42 | CGColorSpaceRelease(colorSpace); 43 | return image; 44 | 45 | } 46 | 47 | CGImageRef createImage(id texture) { 48 | 49 | auto w = texture.width; 50 | auto h = texture.height; 51 | 52 | std::vector imageBytes(4*w*h, 0); 53 | 54 | switch(texture.pixelFormat) { 55 | case MTLPixelFormatRGBA8Unorm: 56 | [texture getBytes:imageBytes.data() bytesPerRow:w * 4 fromRegion:MTLRegionMake2D(0, 0, w, h) mipmapLevel:0]; 57 | break; 58 | case MTLPixelFormatBGRA8Unorm: 59 | [texture getBytes:imageBytes.data() bytesPerRow:w * 4 fromRegion:MTLRegionMake2D(0, 0, w, h) mipmapLevel:0]; 60 | for(auto i=0;i tmpBytes(w*h); 66 | [texture getBytes:tmpBytes.data() bytesPerRow:w fromRegion:MTLRegionMake2D(0, 0, w, h) mipmapLevel:0]; 67 | for(auto i=0;i texture, NSString* name) { 81 | 82 | auto tmpURL = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:name]; 83 | NSLog(@"saving to %@", tmpURL); 84 | CGImageRef image = createImage(texture); 85 | writeCGImage(image, (__bridge CFURLRef)tmpURL); 86 | #if TARGET_OS_OSX 87 | // system([NSString stringWithFormat:@"open %@", tmpURL.path].UTF8String); 88 | auto task = [[NSTask alloc] init]; 89 | task.launchPath = @"/usr/bin/open"; 90 | task.arguments = @[tmpURL.path]; 91 | [task launch]; 92 | #endif 93 | CGImageRelease(image); 94 | 95 | } 96 | 97 | @interface checkTextureBundleFinder : NSObject 98 | 99 | @end 100 | 101 | @implementation checkTextureBundleFinder 102 | 103 | @end 104 | 105 | bool checkTexture(id texture, NSString* name) { 106 | 107 | auto tmpURL = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:name]; 108 | NSLog(@"saving to %@", tmpURL); 109 | CGImageRef image = createImage(texture); 110 | writeCGImage(image, (__bridge CFURLRef)tmpURL); 111 | CGImageRelease(image); 112 | 113 | // Get URL for baseline. 114 | NSString* path = @"Contents/Resources/vger_vgerTests.bundle/Contents/Resources/images/"; 115 | path = [path stringByAppendingString:name]; 116 | NSBundle* bundle = [NSBundle bundleForClass:checkTextureBundleFinder.class]; 117 | auto baselineURL = [bundle.bundleURL URLByAppendingPathComponent:path]; 118 | NSLog(@"checking against baseline %@", baselineURL); 119 | 120 | bool equal = [NSFileManager.defaultManager contentsEqualAtPath:baselineURL.path andPath:tmpURL.path]; 121 | 122 | if(!equal) { 123 | #if TARGET_OS_OSX 124 | system([NSString stringWithFormat:@"open %@", tmpURL.path].UTF8String); 125 | #endif 126 | } 127 | 128 | return equal; 129 | } 130 | -------------------------------------------------------------------------------- /Sources/vger/vgerRenderer.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import "vgerRenderer.h" 4 | #import "paint.h" 5 | #import "prim.h" 6 | 7 | static id GetMetalLibrary(id device) { 8 | 9 | auto bundle = SWIFTPM_MODULE_BUNDLE; 10 | assert(bundle); 11 | 12 | auto libraryURL = [bundle URLForResource:@"default" withExtension:@"metallib"]; 13 | 14 | NSError* error; 15 | auto lib = [device newLibraryWithURL:libraryURL error:&error]; 16 | if(error) { 17 | NSLog(@"error creating metal library: %@", lib); 18 | } 19 | 20 | assert(lib); 21 | return lib; 22 | } 23 | 24 | @interface vgerRenderer() { 25 | id pipeline; 26 | id boundsPipeline; 27 | } 28 | @end 29 | 30 | @implementation vgerRenderer 31 | 32 | - (instancetype)initWithDevice:(id) device 33 | pixelFormat:(MTLPixelFormat) pixelFormat { 34 | self = [super init]; 35 | if (self) { 36 | auto lib = GetMetalLibrary(device); 37 | 38 | auto desc = [MTLRenderPipelineDescriptor new]; 39 | desc.vertexFunction = [lib newFunctionWithName:@"vger_vertex"]; 40 | desc.fragmentFunction = [lib newFunctionWithName:@"vger_fragment"]; 41 | 42 | auto ad = desc.colorAttachments[0]; 43 | ad.pixelFormat = pixelFormat; 44 | ad.blendingEnabled = true; 45 | ad.sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; 46 | ad.sourceAlphaBlendFactor = MTLBlendFactorSourceAlpha; 47 | ad.destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; 48 | ad.destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; 49 | 50 | NSError* error; 51 | pipeline = [device newRenderPipelineStateWithDescriptor:desc error:&error]; 52 | if(error) { 53 | NSLog(@"error creating pipline state: %@", error); 54 | abort(); 55 | } 56 | 57 | auto boundsFunc = [lib newFunctionWithName:@"vger_bounds"]; 58 | boundsPipeline = [device newComputePipelineStateWithFunction:boundsFunc error:&error]; 59 | if(error) { 60 | NSLog(@"error creating pipline state: %@", error); 61 | abort(); 62 | } 63 | } 64 | return self; 65 | } 66 | 67 | - (void) encodeTo:(id) buffer 68 | pass:(MTLRenderPassDescriptor*) pass 69 | scene:(const vgerScene&) scene 70 | count:(int)n 71 | layer:(int)layer 72 | textures:(NSArray>*)textures 73 | glyphTexture:(id)glyphTexture 74 | windowSize:(vector_float2)windowSize 75 | glow:(bool)glow 76 | { 77 | if(n == 0) { 78 | return; 79 | } 80 | 81 | auto bounds = [buffer computeCommandEncoder]; 82 | bounds.label = @"bounds encoder"; 83 | [bounds setComputePipelineState:boundsPipeline]; 84 | [bounds setBuffer:scene.prims[layer].buffer offset:0 atIndex:0]; 85 | [bounds setBuffer:scene.cvs.buffer offset:0 atIndex:1]; 86 | [bounds setBytes:&n length:sizeof(uint) atIndex:2]; 87 | [bounds dispatchThreadgroups:MTLSizeMake(n/128+1, 1, 1) 88 | threadsPerThreadgroup:MTLSizeMake(128, 1, 1)]; 89 | [bounds endEncoding]; 90 | 91 | auto enc = [buffer renderCommandEncoderWithDescriptor:pass]; 92 | enc.label = @"render encoder"; 93 | 94 | [enc setRenderPipelineState:pipeline]; 95 | [enc setFragmentTexture:glyphTexture atIndex:1]; 96 | [enc setVertexBuffer:scene.prims[layer].buffer offset:0 atIndex:0]; 97 | [enc setVertexBuffer:scene.xforms.buffer offset:0 atIndex:1]; 98 | [enc setVertexBytes:&windowSize length:sizeof(windowSize) atIndex:2]; 99 | [enc setFragmentBuffer:scene.prims[layer].buffer offset:0 atIndex:0]; 100 | [enc setFragmentBuffer:scene.cvs.buffer offset:0 atIndex:1]; 101 | [enc setFragmentBuffer:scene.paints.buffer offset:0 atIndex:2]; 102 | [enc setFragmentBytes:&glow length:sizeof(bool) atIndex:3]; 103 | 104 | vgerPrim* p = scene.prims[layer].ptr; 105 | vgerPaint* paints = (vgerPaint*) scene.paints.ptr; 106 | int currentTexture = -1; 107 | int m = 0; 108 | int offset = 0; 109 | for(int i=0;ipaint < scene.paints.count); 112 | int imageID = paints[p->paint].image; 113 | 114 | // Texture ID changed, render. 115 | if(imageID >= 0 and imageID != currentTexture) { 116 | 117 | if(m) { 118 | [enc setVertexBufferOffset:offset atIndex:0]; 119 | [enc setFragmentBufferOffset:offset atIndex:0]; 120 | [enc drawPrimitives:MTLPrimitiveTypeTriangleStrip 121 | vertexStart:0 122 | vertexCount:4 123 | instanceCount:m]; 124 | } 125 | 126 | assert(imageID < textures.count); 127 | [enc setFragmentTexture:[textures objectAtIndex:imageID] atIndex:0]; 128 | 129 | currentTexture = imageID; 130 | offset = i*sizeof(vgerPrim); 131 | m = 0; 132 | } 133 | 134 | p++; m++; 135 | } 136 | 137 | if(m) { 138 | [enc setVertexBufferOffset:offset atIndex:0]; 139 | [enc setFragmentBufferOffset:offset atIndex:0]; 140 | [enc drawPrimitives:MTLPrimitiveTypeTriangleStrip 141 | vertexStart:0 142 | vertexCount:4 143 | instanceCount:m]; 144 | } 145 | 146 | [enc endEncoding]; 147 | 148 | } 149 | 150 | @end 151 | -------------------------------------------------------------------------------- /Sources/vger/vger.metal: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #include 4 | using namespace metal; 5 | 6 | #include "include/vger.h" 7 | #include "sdf.h" 8 | #include "paint.h" 9 | 10 | #define SQRT_2 1.414213562373095 11 | 12 | struct VertexOut { 13 | float4 position [[ position ]]; 14 | float2 t; 15 | int primIndex; 16 | }; 17 | 18 | float sdPrim(const DEVICE vgerPrim& prim, const DEVICE float2* cvs, float2 p, float filterWidth = 0) { 19 | float d = FLT_MAX; 20 | float s = 1; 21 | switch(prim.type) { 22 | case vgerBezier: 23 | d = udBezierApprox(p, prim.cvs[0], prim.cvs[1], prim.cvs[2]) - prim.width; 24 | break; 25 | case vgerCircle: 26 | d = sdCircle(p - prim.cvs[0], prim.radius); 27 | break; 28 | case vgerArc: 29 | d = sdArc2(p - prim.cvs[0], prim.cvs[1], prim.cvs[2], prim.radius, prim.width/2); 30 | break; 31 | case vgerRect: 32 | case vgerGlyph: { 33 | auto center = .5*(prim.cvs[1] + prim.cvs[0]); 34 | auto size = prim.cvs[1] - prim.cvs[0]; 35 | d = sdBox(p - center, .5*size, prim.radius); 36 | } 37 | break; 38 | case vgerRectStroke: { 39 | auto center = .5*(prim.cvs[1] + prim.cvs[0]); 40 | auto size = prim.cvs[1] - prim.cvs[0]; 41 | d = abs(sdBox(p - center, .5*size, prim.radius)) - prim.width/2; 42 | } 43 | break; 44 | case vgerSegment: 45 | d = sdSegment2(p, prim.cvs[0], prim.cvs[1], prim.width); 46 | break; 47 | case vgerCurve: 48 | for(int i=0; i xmax and b.x > xmax and c.x > xmax) { 70 | close = false; 71 | } else if(a.x < xmin and b.x < xmin and c.x < xmin) { 72 | close = false; 73 | } 74 | 75 | if(close) { 76 | d = min(d, udBezier(p, a, b, c)); 77 | 78 | // Flip if inside area between curve and line. 79 | if(bezierTest(p, a, b, c)) { 80 | s = -s; 81 | } 82 | } 83 | 84 | if(lineTest(p, a, c)) { 85 | s = -s; 86 | } 87 | 88 | } 89 | d *= s; 90 | break; 91 | default: 92 | break; 93 | } 94 | return d; 95 | } 96 | 97 | /// Calculates bounds for prims. 98 | kernel void vger_bounds(uint gid [[thread_position_in_grid]], 99 | device vgerPrim* prims, 100 | const device float2* cvs, 101 | constant uint& primCount) { 102 | 103 | if(gid < primCount) { 104 | device auto& p = prims[gid]; 105 | 106 | if(p.type != vgerGlyph and p.type != vgerPathFill) { 107 | 108 | auto bounds = sdPrimBounds(p, cvs).inset(-1); 109 | p.quadBounds[0] = p.texBounds[0] = bounds.min; 110 | p.quadBounds[1] = p.texBounds[1] = bounds.max; 111 | 112 | } 113 | } 114 | } 115 | 116 | /// Removes a prim if its texture region is outside the rendered geometry. 117 | kernel void vger_prune(uint gid [[thread_position_in_grid]], 118 | device vgerPrim* prims, 119 | const device float2* cvs, 120 | constant uint& primCount) { 121 | 122 | if(gid < primCount) { 123 | device auto& prim = prims[gid]; 124 | 125 | auto center = 0.5 * (prim.texBounds[0] + prim.texBounds[1]); 126 | auto tile_size = prim.texBounds[1] - prim.texBounds[0]; 127 | 128 | if(sdPrim(prim, cvs, center) > max(tile_size.x, tile_size.y) * 0.5 * SQRT_2) { 129 | float2 big = {FLT_MAX, FLT_MAX}; 130 | prim.quadBounds[0] = big; 131 | prim.quadBounds[1] = big; 132 | } 133 | 134 | } 135 | } 136 | 137 | vertex VertexOut vger_vertex(uint vid [[vertex_id]], 138 | uint iid [[instance_id]], 139 | const device vgerPrim* prims, 140 | const device float3x3* xforms, 141 | constant float2& viewSize) { 142 | 143 | device auto& prim = prims[iid]; 144 | 145 | VertexOut out; 146 | out.primIndex = iid; 147 | out.t = float2(prim.texBounds[vid & 1].x, prim.texBounds[vid >> 1].y); 148 | 149 | auto q = xforms[prim.xform] * float3(float2(prim.quadBounds[vid & 1].x, 150 | prim.quadBounds[vid >> 1].y), 151 | 1.0); 152 | 153 | auto p = float2{q.x/q.z, q.y/q.z}; 154 | out.position = float4(2.0 * p / viewSize - 1.0, 0, 1); 155 | 156 | return out; 157 | } 158 | 159 | float gridAlpha(float3 pos) { 160 | float aa = length(fwidth(pos)); 161 | auto toGrid = abs(pos - round(pos)); 162 | auto dist = min(toGrid.x, toGrid.y); 163 | return 0.5 * smoothstep(aa, 0, dist); 164 | } 165 | 166 | // Adapted from https://www.shadertoy.com/view/MtlcWX 167 | inline float4 applyGrid(const device vgerPaint& paint, float2 p) { 168 | 169 | auto pos = paint.xform * float3{p.x, p.y, 1.0}; 170 | float alpha = gridAlpha(pos); 171 | 172 | auto color = paint.innerColor; 173 | color.a *= alpha; 174 | return color; 175 | 176 | } 177 | 178 | fragment float4 vger_fragment(VertexOut in [[ stage_in ]], 179 | const device vgerPrim* prims, 180 | const device float2* cvs, 181 | const device vgerPaint* paints, 182 | constant bool& glow, 183 | texture2d tex, 184 | texture2d glyphs) { 185 | 186 | device auto& prim = prims[in.primIndex]; 187 | device auto& paint = paints[prim.paint]; 188 | 189 | if(prim.type == vgerGlyph) { 190 | 191 | constexpr sampler glyphSampler (mag_filter::linear, 192 | min_filter::linear, 193 | coord::pixel); 194 | 195 | auto c = paint.innerColor; 196 | auto color = float4(c.rgb, c.a * glyphs.sample(glyphSampler, in.t).a); 197 | 198 | if(glow) { 199 | color.a *= paint.glow; 200 | } 201 | 202 | return color; 203 | } 204 | 205 | float fw = length(fwidth(in.t)); 206 | float d = sdPrim(prim, cvs, in.t, fw); 207 | 208 | //if(d > 2*sw) { 209 | // discard_fragment(); 210 | //} 211 | 212 | float4 color; 213 | 214 | if(paint.image == -1) { 215 | color = applyPaint(paint, in.t); 216 | } else if(paint.image == -2) { 217 | color = applyGrid(paint, in.t); 218 | } else { 219 | 220 | constexpr sampler textureSampler (mag_filter::linear, 221 | min_filter::linear); 222 | 223 | auto t = (paint.xform * float3(in.t,1)).xy; 224 | if(!paint.flipY) { 225 | t.y = 1.0 - t.y; 226 | } 227 | color = tex.sample(textureSampler, t); 228 | color.a *= paint.innerColor.a; 229 | } 230 | 231 | if(glow) { 232 | color.a *= paint.glow; 233 | } 234 | 235 | return mix(float4(color.rgb,0.0), color, 1.0-smoothstep(-fw/2,fw/2,d) ); 236 | 237 | } 238 | -------------------------------------------------------------------------------- /Sources/vger/vger_private.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include "vgerPathScanner.h" 9 | #include "vgerGlyphPathCache.h" 10 | #include "vgerScene.h" 11 | #include "paint.h" 12 | 13 | @class vgerRenderer; 14 | @class vgerGlyphCache; 15 | 16 | /// For caching the layout of strings. 17 | struct TextLayoutInfo { 18 | /// The frame in which the string was last rendered. If not the current frame, 19 | /// then the string is pruned from the cache. 20 | uint64_t lastFrame = 0; 21 | 22 | /// Prims are copied to output. 23 | std::vector prims; 24 | }; 25 | 26 | struct TextLayoutKey { 27 | std::string str; 28 | float size; 29 | int align; 30 | float breakRowWidth = -1; 31 | 32 | friend bool operator==(const TextLayoutKey&, const TextLayoutKey&) = default; 33 | friend bool operator!=(const TextLayoutKey&, const TextLayoutKey&) = default; 34 | }; 35 | 36 | inline void hash_combine(size_t& seed) { } 37 | 38 | template 39 | inline void hash_combine(size_t& seed, const T& v, Rest... rest) { 40 | std::hash hasher; 41 | seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); 42 | hash_combine(seed, rest...); 43 | } 44 | 45 | #define MAKE_HASHABLE(Type, ...) \ 46 | namespace std {\ 47 | template<> struct hash {\ 48 | size_t operator()(const Type &t) const {\ 49 | size_t ret = 0;\ 50 | hash_combine(ret, __VA_ARGS__);\ 51 | return ret;\ 52 | }\ 53 | };\ 54 | } 55 | 56 | MAKE_HASHABLE(TextLayoutKey, t.str, t.size, t.align, t.breakRowWidth); 57 | 58 | /// Main state object. This is not ObjC to avoid call overhead for each prim. 59 | struct vger { 60 | 61 | id device; 62 | vgerRenderer* renderer; 63 | 64 | vgerRenderer* glowRenderer; 65 | 66 | /// Transform matrix stack. 67 | std::vector txStack; 68 | 69 | /// Number of buffers. 70 | int maxBuffers = 1; 71 | 72 | /// We cycle through three scenes for streaming. 73 | vgerScene scenes[3]; 74 | 75 | /// The current scene we're writing to. 76 | int currentScene = 0; 77 | 78 | /// Current layer we're rendering to. 79 | int currentLayer = 0; 80 | 81 | /// Number of layers. 82 | int layerCount = 1; 83 | 84 | /// Atlas for finding glyph images. 85 | vgerGlyphCache* glyphCache; 86 | 87 | /// Size of rendering window (for conversion from pixel to NDC) 88 | float2 windowSize; 89 | 90 | /// Glyph scratch space (avoid malloc). 91 | std::vector glyphs; 92 | 93 | /// Cache of text layout by strings. 94 | std::unordered_map< TextLayoutKey, TextLayoutInfo > textCache; 95 | 96 | /// Points scratch space (avoid malloc). 97 | std::vector points; 98 | 99 | /// Determines whether we prune cached text. 100 | uint64_t currentFrame = 1; 101 | 102 | /// User-created textures. 103 | NSMutableArray< id >* textures; 104 | 105 | /// We can't insert nil into textures, so use a tiny texture instead. 106 | id nullTexture; 107 | 108 | /// Content scale factor. 109 | float devicePxRatio = 1.0; 110 | 111 | /// For speeding up path rendering. 112 | vgerPathScanner yScanner; 113 | 114 | /// For generating glyph paths. 115 | vgerGlyphPathCache glyphPathCache; 116 | 117 | /// The current location when creating paths. 118 | float2 pen; 119 | 120 | /// For loading images from files. 121 | MTKTextureLoader* textureLoader; 122 | 123 | /// Have we already computed glyph bounds? 124 | bool computedGlyphBounds = false; 125 | 126 | /// Used in vgerTextBounds. 127 | std::vector origins; 128 | 129 | /// Used in vgerStrokeBezier. 130 | std::vector segments; 131 | 132 | /// Used in vgerStrokeBezier. 133 | std::vector top_points, bottom_points; 134 | 135 | vger(uint32_t flags, MTLPixelFormat pixelFormat); 136 | 137 | void addPrim(const vgerPrim& prim) { 138 | scenes[currentScene].prims[currentLayer].append(prim); 139 | } 140 | 141 | auto primCount() -> size_t { 142 | return scenes[currentScene].prims[currentLayer].count; 143 | } 144 | 145 | void addCV(float2 p) { 146 | scenes[currentScene].cvs.append(p); 147 | } 148 | 149 | uint32_t addxform(const matrix_float3x3& M) { 150 | uint32_t idx = (uint32_t) scenes[currentScene].xforms.count; 151 | scenes[currentScene].xforms.append(M); 152 | return idx; 153 | } 154 | 155 | vgerPaintIndex addPaint(const vgerPaint& paint) { 156 | uint32_t idx = (uint32_t) scenes[currentScene].paints.count; 157 | scenes[currentScene].paints.append(paint); 158 | return {idx}; 159 | } 160 | 161 | /// Ensure a paint index is valid. 162 | auto checkPaint(vgerPaintIndex index) -> bool { 163 | return index.index < scenes[currentScene].paints.count; 164 | } 165 | 166 | CTLineRef createCTLine(const char* str); 167 | CTFrameRef createCTFrame(const char* str, int align, float breakRowWidth); 168 | 169 | void begin(float windowWidth, float windowHeight, float devicePxRatio); 170 | 171 | bool fill(vgerPaintIndex paint); 172 | 173 | void fillForTile(vgerPaintIndex paint); 174 | 175 | void encode(id buf, MTLRenderPassDescriptor* pass, bool glow); 176 | 177 | void encodeTileRender(id buf, id renderTexture); 178 | 179 | bool renderCachedText(const TextLayoutKey& key, vgerPaintIndex paint, uint32_t xform); 180 | 181 | void renderTextLine(CTLineRef line, TextLayoutInfo& textInfo, vgerPaintIndex paint, float2 offset, float scale, uint32_t xform); 182 | 183 | void renderText(const char* str, float4 color, int align); 184 | 185 | void renderTextBox(const char* str, float breakRowWidth, float4 color, int align); 186 | 187 | void renderGlyphPath(CGGlyph glyph, vgerPaintIndex paint, float2 position, uint32_t xform); 188 | }; 189 | 190 | inline vgerPaint makeLinearGradient(float2 start, 191 | float2 end, 192 | float4 innerColor, 193 | float4 outerColor, 194 | float glow) { 195 | 196 | vgerPaint p; 197 | p.type = vgerPaintTypeLinearGradient; 198 | 199 | // Calculate transform aligned to the line 200 | float2 d = end - start; 201 | if(length(d) < 0.0001f) { 202 | d = float2{0,1}; 203 | } 204 | 205 | p.xform = inverse(float3x3{ 206 | float3{d.x, d.y, 0}, 207 | float3{-d.y, d.x, 0}, 208 | float3{start.x, start.y, 1} 209 | }); 210 | 211 | p.innerColor = innerColor; 212 | p.outerColor = outerColor; 213 | p.image = -1; 214 | p.glow = glow; 215 | 216 | return p; 217 | } 218 | 219 | inline vgerPaint makeRadialGradient(float2 center, 220 | float innerRadius, 221 | float outerRadius, 222 | float4 innerColor, 223 | float4 outerColor, 224 | float glow) { 225 | 226 | vgerPaint p; 227 | p.type = vgerPaintTypeRadialGradient; 228 | 229 | p.xform = inverse(float3x3{ 230 | float3{1, 0, 0}, 231 | float3{0, 1, 0}, 232 | float3{center.x, center.y, 1} 233 | }); 234 | 235 | p.innerRadius = innerRadius; 236 | p.outerRadius = outerRadius; 237 | p.innerColor = innerColor; 238 | p.outerColor = outerColor; 239 | p.image = -1; 240 | p.glow = glow; 241 | 242 | return p; 243 | } 244 | 245 | inline vgerPaint makeImagePattern(float2 origin, 246 | float2 size, 247 | float angle, 248 | bool flipY, 249 | vgerImageIndex image, 250 | float alpha) { 251 | 252 | vgerPaint p; 253 | p.type = vgerPaintTypeImagePattern; 254 | p.image = image.index; 255 | p.flipY = flipY; 256 | 257 | float3x3 R = { 258 | float3{ cosf(angle), sinf(angle), 0 }, 259 | float3{ -sinf(angle), cosf(angle), 0 }, 260 | float3{ -origin.x, -origin.y, 1} 261 | }; 262 | 263 | float3x3 S = { 264 | float3{ 1/size.x, 0, 0 }, 265 | float3{ 0, 1/size.y, 0}, 266 | float3{ 0, 0, 1} 267 | }; 268 | 269 | p.xform = matrix_multiply(S, R); 270 | 271 | p.innerColor = p.outerColor = float4{1,1,1,alpha}; 272 | p.glow = 0; 273 | 274 | return p; 275 | } 276 | -------------------------------------------------------------------------------- /Sources/vger/include/vger.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. 2 | // 3 | // This software is provided 'as-is', without any express or implied 4 | // warranty. In no event will the authors be held liable for any damages 5 | // arising from the use of this software. 6 | // Permission is granted to anyone to use this software for any purpose, 7 | // including commercial applications, and to alter it and redistribute it 8 | // freely, subject to the following restrictions: 9 | // 1. The origin of this software must not be misrepresented; you must not 10 | // claim that you wrote the original software. If you use this software 11 | // in a product, an acknowledgment in the product documentation would be 12 | // appreciated but is not required. 13 | // 2. Altered source versions must be plainly marked as such, and must not be 14 | // misrepresented as being the original software. 15 | // 3. This notice may not be removed or altered from any source distribution. 16 | // 17 | 18 | #ifndef vger_h 19 | #define vger_h 20 | 21 | #ifndef __METAL_VERSION__ 22 | #include 23 | #endif 24 | 25 | /// Text alignment. 26 | enum vgerAlign { 27 | // Horizontal align 28 | VGER_ALIGN_LEFT = 1<<0, // Default, align text horizontally to left. 29 | VGER_ALIGN_CENTER = 1<<1, // Align text horizontally to center. 30 | VGER_ALIGN_RIGHT = 1<<2, // Align text horizontally to right. 31 | // Vertical align 32 | VGER_ALIGN_TOP = 1<<3, // Align text vertically to top. 33 | VGER_ALIGN_MIDDLE = 1<<4, // Align text vertically to middle. 34 | VGER_ALIGN_BOTTOM = 1<<5, // Align text vertically to bottom. 35 | VGER_ALIGN_BASELINE = 1<<6, // Default, align text vertically to baseline. 36 | }; 37 | 38 | /// Type safety for paint indices. 39 | typedef struct { uint32_t index; } vgerPaintIndex; 40 | 41 | /// Type safety for image indices. 42 | typedef struct { uint32_t index; } vgerImageIndex; 43 | 44 | #ifndef __METAL_VERSION__ 45 | 46 | #ifdef __OBJC__ 47 | #import 48 | #endif 49 | 50 | #ifdef __cplusplus 51 | extern "C" { 52 | #endif 53 | 54 | typedef struct vger *vgerContext; 55 | 56 | #pragma mark - Context 57 | 58 | enum vgerCreateFlags { 59 | // Flag indicating if double buffering scheme is used. 60 | VGER_DOUBLE_BUFFER = 1 << 0, 61 | 62 | // Flag indicating if triple buffering scheme is used. 63 | VGER_TRIPLE_BUFFER = 1 << 1, 64 | }; 65 | 66 | #ifdef __OBJC__ 67 | /// Create a new state object. 68 | vgerContext vgerNew(uint32_t flags, MTLPixelFormat pixelFormat); 69 | #endif 70 | 71 | /// Deallocate state object. 72 | void vgerDelete(vgerContext); 73 | 74 | /// Begin rendering a frame. 75 | void vgerBegin(vgerContext, float windowWidth, float windowHeight, float devicePxRatio); 76 | 77 | #pragma mark - Textures 78 | 79 | /// Load an image from a file. 80 | vgerImageIndex vgerCreateImage(vgerContext, const char* filename); 81 | 82 | /// Load an image from data in memory. 83 | vgerImageIndex vgerCreateImageMem(vgerContext, const uint8_t* data, size_t size); 84 | 85 | /// Create a texture to sample from. 86 | vgerImageIndex vgerAddTexture(vgerContext, const uint8_t* data, int width, int height); 87 | 88 | #ifdef __OBJC__ 89 | /// Add a MTLTexture. This references the texture, so you can render to it, etc. 90 | vgerImageIndex vgerAddMTLTexture(vgerContext, id); 91 | #endif 92 | 93 | /// Remove a texture. 94 | void vgerDeleteTexture(vgerContext, vgerImageIndex texID); 95 | 96 | /// Remove all textures. 97 | void vgerDeleteTextures(vgerContext); 98 | 99 | /// Get the size of a texture. 100 | vector_int2 vgerTextureSize(vgerContext, vgerImageIndex texID); 101 | 102 | #pragma mark - Primitives 103 | 104 | void vgerFillCircle(vgerContext, vector_float2 center, float radius, vgerPaintIndex paint); 105 | 106 | void vgerStrokeArc(vgerContext, vector_float2 center, float radius, float width, float rotation, float aperture, vgerPaintIndex paint); 107 | 108 | void vgerFillRect(vgerContext, vector_float2 min, vector_float2 max, float radius, vgerPaintIndex paint); 109 | 110 | void vgerStrokeRect(vgerContext, vector_float2 min, vector_float2 max, float radius, float width, vgerPaintIndex paint); 111 | 112 | typedef struct { vector_float2 a, b, c; } vgerBezierSegment; 113 | 114 | void vgerStrokeBezier(vgerContext, vgerBezierSegment, float width, vgerPaintIndex paint); 115 | 116 | void vgerStrokeSegment(vgerContext, vector_float2 a, vector_float2 b, float width, vgerPaintIndex paint); 117 | 118 | void vgerStrokeWire(vgerContext, vector_float2 a, vector_float2 b, float width, vgerPaintIndex paint); 119 | 120 | /// Returns the number of primitives sent for rendering. 121 | /// 122 | /// This can be useful if you want to impose a limit. 123 | size_t vgerPrimCount(vgerContext); 124 | 125 | #pragma mark - Text 126 | 127 | /// Render text. 128 | void vgerText(vgerContext, const char* str, vector_float4 color, int align); 129 | 130 | /// Return bounds for text in local coordinates. 131 | void vgerTextBounds(vgerContext, const char* str, vector_float2* min, vector_float2* max, int align); 132 | 133 | /// Renders multi-line text. 134 | void vgerTextBox(vgerContext, const char* str, float breakRowWidth, vector_float4 color, int align); 135 | 136 | /// Returns bounds of multi-line text. 137 | void vgerTextBoxBounds(vgerContext, const char* str, float breakRowWidth, vector_float2* min, vector_float2* max, int align); 138 | 139 | #pragma mark - Paths 140 | 141 | /// Move the pen to a point. 142 | void vgerMoveTo(vgerContext, vector_float2 pt); 143 | 144 | /// Line. 145 | void vgerLineTo(vgerContext, vector_float2 b); 146 | 147 | /// Quadratic bezier. 148 | void vgerQuadTo(vgerContext, vector_float2 b, vector_float2 c); 149 | 150 | /// Crude approximation of cubic bezier with two quadratics. 151 | void vgerCubicApproxTo(vgerContext vg, vector_float2 b, vector_float2 c, vector_float2 d); 152 | 153 | /// Fills the current path (and clears the path). 154 | /// 155 | /// Returns false if there were too many primitives to render the fill. 156 | bool vgerFill(vgerContext, vgerPaintIndex paint); 157 | 158 | /// Clear path information. 159 | /// 160 | /// This is useful for ensuring scripts don't mess up other rendering. 161 | void vgerCancelPath(vgerContext vg); 162 | 163 | #pragma mark - Transforms 164 | 165 | /// Translates current coordinate system. 166 | void vgerTranslate(vgerContext, vector_float2 t); 167 | 168 | /// Scales current coordinate system. 169 | void vgerScale(vgerContext, vector_float2 s); 170 | 171 | /// Rotates current coordinate system (radians). 172 | void vgerRotate(vgerContext, float theta); 173 | 174 | /// Transforms a point according to the current transformation. 175 | vector_float2 vgerTransform(vgerContext, vector_float2 p); 176 | 177 | /// Returns current transformation matrix. 178 | simd_float3x2 vgerCurrentTransform(vgerContext); 179 | 180 | /// Pushes and saves the current transform onto a transform stack. A matching vgerRestore must 181 | /// be used to restore the state. 182 | void vgerSave(vgerContext); 183 | 184 | /// Pops and restores the current transform. 185 | void vgerRestore(vgerContext); 186 | 187 | /// Returns the depth of the transform stack. 188 | size_t vgerStackDepth(vgerContext); 189 | 190 | #pragma mark - Layers 191 | 192 | /// Sets the number of layers (currently the max is 4). Default is 1. 193 | void vgerSetLayerCount(vgerContext, int layers); 194 | 195 | /// Sets the current layer index. 196 | void vgerSetLayer(vgerContext, int layer); 197 | 198 | #pragma mark - Encoding 199 | 200 | #ifdef __OBJC__ 201 | /// Encode drawing commands to a metal command buffer. 202 | void vgerEncode(vgerContext, id buf, MTLRenderPassDescriptor* pass); 203 | 204 | /// Encode drawing commands to a metal command buffer for the glow pass. 205 | void vgerEncodeGlowPass(vgerContext, id buf, MTLRenderPassDescriptor* pass); 206 | 207 | /// For debugging. 208 | id vgerGetGlyphAtlas(vgerContext); 209 | 210 | /// For debugging. 211 | id vgerGetCoarseDebugTexture(vgerContext); 212 | #endif 213 | 214 | #pragma mark - Paints 215 | 216 | /// Create a paint for a constant color. Returns paint index. Paints are cleared each frame. 217 | vgerPaintIndex vgerColorPaint(vgerContext vg, vector_float4 color); 218 | 219 | /// Create a paint for a linear gradient. Returns paint index. Paints are cleared each frame. 220 | vgerPaintIndex vgerLinearGradient(vgerContext vg, 221 | vector_float2 start, 222 | vector_float2 end, 223 | vector_float4 innerColor, 224 | vector_float4 outerColor, 225 | float glow); 226 | 227 | /// Create a paint for a linear gradient. Returns paint index. Paints are cleared each frame. 228 | vgerPaintIndex vgerRadialGradient(vgerContext vg, 229 | vector_float2 center, 230 | float innerRadius, 231 | float outerRadius, 232 | vector_float4 innerColor, 233 | vector_float4 outerColor, 234 | float glow); 235 | 236 | /// Create a paint using a texture image. Returns paint index. Paints are cleared each frame. 237 | vgerPaintIndex vgerImagePattern(vgerContext vg, 238 | vector_float2 origin, 239 | vector_float2 size, 240 | float angle, 241 | bool flipY, 242 | vgerImageIndex image, 243 | float alpha); 244 | 245 | /// Create a paint to render a grid. Reduces the number of primitives required for a big grid. 246 | vgerPaintIndex vgerGrid(vgerContext, vector_float2 offset, vector_float2 size, float width, vector_float4 color); 247 | 248 | #ifdef __cplusplus 249 | } 250 | #endif 251 | 252 | #endif // __METAL_VERSION__ 253 | 254 | #endif /* vger_h */ 255 | -------------------------------------------------------------------------------- /Sources/vger/sdf.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include "metal_compat.h" 6 | #include "prim.h" 7 | 8 | // Projection of b onto a. 9 | inline float2 proj(float2 a, float2 b) { 10 | return normalize(a) * dot(a,b) / length(a); 11 | } 12 | 13 | inline float2 orth(float2 a, float2 b) { 14 | return b - proj(a, b); 15 | } 16 | 17 | inline float2 rot90(float2 p) { 18 | return {-p.y, p.x}; 19 | } 20 | 21 | // From https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm 22 | // See also https://www.shadertoy.com/view/4dfXDn 23 | 24 | inline float sdCircle( float2 p, float r ) 25 | { 26 | return length(p) - r; 27 | } 28 | 29 | inline float sdBox( float2 p, float2 b, float r ) 30 | { 31 | float2 d = abs(p)-b+r; 32 | return length(max(d,float2(0.0))) + min(max(d.x,d.y),0.0)-r; 33 | } 34 | 35 | inline float sdSegment(float2 p, float2 a, float2 b ) 36 | { 37 | float2 pa = p-a, ba = b-a; 38 | float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 ); 39 | return length( pa - ba*h ); 40 | } 41 | 42 | inline float sdSegment2(float2 p, float2 a, float2 b, float width) 43 | { 44 | float2 u = normalize(b-a); 45 | float2 v = rot90(u); 46 | 47 | p -= (a+b)/2; 48 | p = p * float2x2{u, v}; 49 | return sdBox(p, float2{length(b-a)/2, width/2}, 0); 50 | } 51 | 52 | // sca is {sin,cos} of orientation 53 | // scb is {sin,cos} of aperture angle 54 | inline float sdArc(float2 p, float2 sca, float2 scb, float ra, float rb ) 55 | { 56 | p = p * float2x2{float2{sca.x,sca.y},float2{-sca.y,sca.x}}; 57 | p.x = abs(p.x); 58 | float k = (scb.y*p.x>scb.x*p.y) ? dot(p,scb) : length(p); 59 | return sqrt( dot(p,p) + ra*ra - 2.0*ra*k ) - rb; 60 | } 61 | 62 | inline float sdHorseshoe(float2 p, float2 c, float r, float2 w ) 63 | { 64 | p.x = abs(p.x); 65 | float l = length(p); 66 | p = float2x2{float2{-c.x, c.y},float2{c.y, c.x}}*p; 67 | p = float2{(p.y>0.0)?p.x:l*sign(-c.x), (p.x>0.0)?p.y:l }; 68 | p = float2{p.x,abs(p.y-r)}-w; 69 | return length(max(p,0.0f)) + min(0.0f,max(p.x,p.y)); 70 | } 71 | 72 | inline float dot2(float2 v) { 73 | return dot(v,v); 74 | } 75 | 76 | /// Unsigned distance to quadratic bezier curve. 77 | inline float udBezier(float2 pos, float2 A, float2 B, float2 C ) 78 | { 79 | // Are the points collinear? 80 | auto M = float2x2{C-A, B-A}; 81 | if (fabs(determinant(M)) < 0.01) { 82 | return sdSegment(pos, A, C); 83 | } 84 | 85 | float2 a = B - A; 86 | float2 b = A - 2.0*B + C; 87 | float2 c = a * 2.0; 88 | float2 d = A - pos; 89 | float kk = 1.0/dot(b,b); 90 | float kx = kk * dot(a,b); 91 | float ky = kk * (2.0*dot(a,a)+dot(d,b)) / 3.0; 92 | float kz = kk * dot(d,a); 93 | float res = 0.0; 94 | float p = ky - kx*kx; 95 | float p3 = p*p*p; 96 | float q = kx*(2.0*kx*kx-3.0*ky) + kz; 97 | float h = q*q + 4.0*p3; 98 | if( h >= 0.0) 99 | { 100 | h = sqrt(h); 101 | float2 x = (float2{h,-h}-q)/2.0; 102 | float2 uv = sign(x)*pow(abs(x), float2(1.0/3.0)); 103 | float t = clamp( uv.x+uv.y-kx, 0.0, 1.0 ); 104 | res = dot2(d + (c + b*t)*t); 105 | } 106 | else 107 | { 108 | float z = sqrt(-p); 109 | float v = acos( q/(p*z*2.0) ) / 3.0; 110 | float m = cos(v); 111 | float n = sin(v)*1.732050808; 112 | float3 t = clamp(float3{m+m,-n-m,n-m}*z-kx,0.0,1.0); 113 | res = min( dot2(d+(c+b*t.x)*t.x), 114 | dot2(d+(c+b*t.y)*t.y) ); 115 | // the third root cannot be the closest 116 | // res = min(res,dot2(d+(c+b*t.z)*t.z)); 117 | } 118 | return sqrt( res ); 119 | } 120 | 121 | inline float3 solveCubic(float a, float b, float c) 122 | { 123 | float p = b - a*a / 3.0, p3 = p*p*p; 124 | float q = a * (2.0*a*a - 9.0*b) / 27.0 + c; 125 | float d = q*q + 4.0*p3 / 27.0; 126 | float offset = -a / 3.0; 127 | if(d >= 0.0) { 128 | float z = sqrt(d); 129 | float2 x = (float2{z, -z} - q) / 2.0; 130 | float2 uv = sign(x)*pow(abs(x), float2(1.0/3.0)); 131 | return float3(offset + uv.x + uv.y); 132 | } 133 | float v = acos(-sqrt(-27.0 / p3) * q / 2.0) / 3.0; 134 | float m = cos(v), n = sin(v)*1.732050808; 135 | return float3{m + m, -n - m, n - m} * sqrt(-p / 3.0) + offset; 136 | } 137 | 138 | inline float sdBezier(float2 p, float2 A, float2 B, float2 C) 139 | { 140 | // Offset to avoid degeneracy when B == midpoint(A, C) 141 | B = mix(B + float2(1e-4), B, step(float2(1e-6f), abs(B * 2.0 - A - C))); 142 | 143 | float2 a = B - A; 144 | float2 b = A - 2.0 * B + C; 145 | float2 c = 2.0 * a; 146 | float2 d = A - p; 147 | 148 | float3 k = float3{3.0f * dot(a, b), 149 | 2.0f * dot(a, a) + dot(d, b), 150 | dot(d, a)} / dot(b, b); 151 | 152 | float3 t = clamp(solveCubic(k.x, k.y, k.z), 0.0, 1.0); 153 | 154 | float minDist = 1e10; 155 | float bestT = 0.0; 156 | for (int i = 0; i < 3; ++i) { 157 | float ti = i == 0 ? t.x : (i == 1 ? t.y : t.z); 158 | float2 qi = A + (c + b * ti) * ti; // Evaluate position on curve 159 | float di = length(qi - p); 160 | if (di < minDist) { 161 | minDist = di; 162 | bestT = ti; 163 | } 164 | } 165 | 166 | // Evaluate curve and tangent at bestT 167 | float2 pos = A + (c + b * bestT) * bestT; 168 | float2 tangent = normalize(2.0 * mix(B - A, C - B, bestT)); 169 | float2 diff = normalize(pos - p); 170 | 171 | return minDist * sign(tangent.x * diff.y - tangent.y * diff.x); 172 | } 173 | 174 | inline float sdSubtract(float d1, float d2) 175 | { 176 | return max(-d1, d2); 177 | } 178 | 179 | inline float sdPie(float2 p, float2 n) 180 | { 181 | return abs(p).x * n.y + p.y*n.x; 182 | } 183 | 184 | /// Arc with square ends. 185 | inline float sdArc2(float2 p, float2 sca, float2 scb, float radius, float width) 186 | { 187 | // Rotate point. 188 | p = p * float2x2{float2{sca.x,sca.y},float2{-sca.y,sca.x}}; 189 | return sdSubtract(sdPie(p, float2{scb.x, -scb.y}), 190 | abs(sdCircle(p, radius)) - width); 191 | } 192 | 193 | inline float sdLine(float2 p, float2 a, float2 b) { 194 | auto d = length(orth(b-a, p-a)); 195 | auto M = float2x2{b-a, p-a}; 196 | return determinant(M) >= 0 ? d : -d; 197 | } 198 | 199 | // From https://www.shadertoy.com/view/4sySDK 200 | 201 | inline float2x2 inv(float2x2 M) { 202 | return (1.0f / determinant(M)) * float2x2{ 203 | float2{M.columns[1][1], -M.columns[0][1]}, 204 | float2{-M.columns[1][0], M.columns[0][0]}}; 205 | } 206 | 207 | inline float sdBezier2(float2 uv, float2 p0, float2 p1, float2 p2){ 208 | 209 | const float2x2 trf1 = float2x2{ float2{-1, 2}, float2{1, 2} }; 210 | auto M = float2x2{p0-p1, p2-p1}; 211 | 212 | if (determinant(M) == 0.0) { 213 | return sdLine(uv, p0, p2); 214 | } 215 | 216 | float2x2 trf2 = inv(M); 217 | float2x2 trf=trf1*trf2; 218 | 219 | uv-=p1; 220 | float2 xy=trf*uv; 221 | xy.y-=1.; 222 | 223 | float2 gradient; 224 | gradient.x=2.*trf.columns[0][0]*(trf.columns[0][0]*uv.x+trf.columns[1][0]*uv.y)-trf.columns[0][1]; 225 | gradient.y=2.*trf.columns[1][0]*(trf.columns[0][0]*uv.x+trf.columns[1][0]*uv.y)-trf.columns[1][1]; 226 | 227 | return (xy.x*xy.x-xy.y)/length(gradient); 228 | } 229 | 230 | inline float det(float2 a, float2 b) { return a.x*b.y-b.x*a.y; } 231 | 232 | inline float2 closestPointInSegment( float2 a, float2 b ) 233 | { 234 | float2 ba = b - a; 235 | return a + ba*clamp( -dot(a,ba)/dot(ba,ba), 0.0, 1.0 ); 236 | } 237 | 238 | // From: http://research.microsoft.com/en-us/um/people/hoppe/ravg.pdf 239 | inline float2 get_distance_vector(float2 b0, float2 b1, float2 b2) { 240 | 241 | float a=det(b0,b2), b=2.0*det(b1,b0), d=2.0*det(b2,b1); 242 | 243 | float f=b*d-a*a; 244 | float2 d21=b2-b1, d10=b1-b0, d20=b2-b0; 245 | float2 gf=2.0*(b*d21+d*d10+a*d20); 246 | gf=float2{gf.y,-gf.x}; 247 | float2 pp=-f*gf/dot(gf,gf); 248 | float2 d0p=b0-pp; 249 | float ap=det(d0p,d20), bp=2.0*det(d10,d0p); 250 | // (note that 2*ap+bp+dp=2*a+b+d=4*area(b0,b1,b2)) 251 | float t=clamp((ap+bp)/(2.0*a+b+d), 0.0 ,1.0); 252 | return mix(mix(b0,b1,t),mix(b1,b2,t),t); 253 | 254 | } 255 | 256 | inline float udBezierApprox(float2 p, float2 A, float2 B, float2 C) { 257 | 258 | float2 v0 = normalize(B - A), v1 = normalize(C - A); 259 | float det = v0.x * v1.y - v1.x * v0.y; 260 | if(abs(det) < 0.01) { 261 | return udBezier(p, A, B, C); 262 | } 263 | 264 | return length(get_distance_vector(A-p, B-p, C-p)); 265 | } 266 | 267 | inline float sdBezierApprox2(float2 p, float2 A, float2 B, float2 C) { 268 | return length(get_distance_vector(A-p, B-p, C-p)); 269 | } 270 | 271 | inline float sdWire(float2 p, float2 a, float2 b) { 272 | 273 | float2 sz = b-a; 274 | float2 uv = (p-a)/sz; 275 | float xscale = 5.0f; 276 | float x = uv.x - 0.5; 277 | float y = 0.5*(tanh(xscale * x) + 1); 278 | float c = cosh(xscale * x); 279 | float dydx = xscale / (c * c); 280 | float dy = abs(uv.y - y); 281 | 282 | return sz.y * dy / sqrt(1.0 + dydx * dydx); 283 | } 284 | 285 | #if 0 286 | template 287 | float sdPolygon(float2 p, T v, int num) 288 | { 289 | float d = dot(p-v[0],p-v[0]); 290 | float s = 1.0; 291 | for( int i=0, j=num-1; i=v[i].y, 301 | p.y e.y*w.x ); 303 | if( all(cond) || all(not(cond)) ) s=-s; 304 | } 305 | 306 | return s*sqrt(d); 307 | } 308 | #endif 309 | 310 | /// Axis-aligned bounding box. 311 | struct BBox { 312 | float2 min; 313 | float2 max; 314 | 315 | BBox inset(float d) const { 316 | return {min+d, max-d}; 317 | } 318 | 319 | void expand(float2 p) { 320 | min = simd::min(min, p); 321 | max = simd::max(max, p); 322 | } 323 | 324 | float2 size() const { return max - min; } 325 | }; 326 | 327 | inline BBox sdPrimBounds(const DEVICE vgerPrim& prim, const DEVICE float2* cvs) { 328 | BBox b; 329 | switch(prim.type) { 330 | case vgerBezier: 331 | b = BBox{ 332 | min(min(prim.cvs[0], prim.cvs[1]), prim.cvs[2]), 333 | max(max(prim.cvs[0], prim.cvs[1]), prim.cvs[2]) 334 | }; 335 | break; 336 | case vgerCircle: 337 | case vgerArc: 338 | b = {prim.cvs[0] - prim.radius, prim.cvs[0] + prim.radius}; 339 | break; 340 | case vgerSegment: 341 | case vgerWire: 342 | b = BBox{ 343 | min(prim.cvs[0], prim.cvs[1]), 344 | max(prim.cvs[0], prim.cvs[1]) 345 | }; 346 | break; 347 | case vgerRect: 348 | case vgerRectStroke: 349 | case vgerGlyph: 350 | b = BBox{prim.cvs[0], prim.cvs[1]}; 351 | break; 352 | case vgerCurve: 353 | case vgerPathFill: { 354 | b = {FLT_MAX, -FLT_MAX}; 355 | for(int i=0;i p.x; 437 | } 438 | 439 | inline bool lineTest(float2 p, float2 A, float2 B) { 440 | 441 | int cs = (A.y < p.y) * 2 + (B.y < p.y); 442 | 443 | if(cs == 0 or cs == 3) return false; // trivial reject 444 | 445 | return side(p, A, B); 446 | 447 | } 448 | 449 | /// Is the point within the area between the curve and line segment A C? 450 | inline bool bezierTest(float2 p, float2 A, float2 B, float2 C) { 451 | 452 | // Compute barycentric coordinates of p. 453 | // p = s * A + t * B + (1-s-t) * C 454 | float2 v0 = B - A, v1 = C - A, v2 = p - A; 455 | float det = v0.x * v1.y - v1.x * v0.y; 456 | float s = (v2.x * v1.y - v1.x * v2.y) / det; 457 | float t = (v0.x * v2.y - v2.x * v0.y) / det; 458 | 459 | // Concave or convex? 460 | // Try to be consistent with lineTest so we don't 461 | // double-flip the inside/outside in edge cases. 462 | if (side(B, A, C)) { 463 | if(!side(p, A, C)) { 464 | return false; 465 | } 466 | } else { 467 | if(side(p, A, C)) { 468 | return false; 469 | } 470 | } 471 | 472 | // Are we outside edge (A, B) or (B, C)? 473 | if(s < 0 or (1-s-t) < 0) { 474 | return false; // outside triangle 475 | } 476 | 477 | // Transform to canonical coordinte space. 478 | float u = s * .5 + t; 479 | float v = t; 480 | 481 | return u*u < v; 482 | 483 | } 484 | -------------------------------------------------------------------------------- /Sources/vger/stb_rect_pack.h: -------------------------------------------------------------------------------- 1 | // stb_rect_pack.h - v1.01 - public domain - rectangle packing 2 | // Sean Barrett 2014 3 | // 4 | // Useful for e.g. packing rectangular textures into an atlas. 5 | // Does not do rotation. 6 | // 7 | // Before #including, 8 | // 9 | // #define STB_RECT_PACK_IMPLEMENTATION 10 | // 11 | // in the file that you want to have the implementation. 12 | // 13 | // Not necessarily the awesomest packing method, but better than 14 | // the totally naive one in stb_truetype (which is primarily what 15 | // this is meant to replace). 16 | // 17 | // Has only had a few tests run, may have issues. 18 | // 19 | // More docs to come. 20 | // 21 | // No memory allocations; uses qsort() and assert() from stdlib. 22 | // Can override those by defining STBRP_SORT and STBRP_ASSERT. 23 | // 24 | // This library currently uses the Skyline Bottom-Left algorithm. 25 | // 26 | // Please note: better rectangle packers are welcome! Please 27 | // implement them to the same API, but with a different init 28 | // function. 29 | // 30 | // Credits 31 | // 32 | // Library 33 | // Sean Barrett 34 | // Minor features 35 | // Martins Mozeiko 36 | // github:IntellectualKitty 37 | // 38 | // Bugfixes / warning fixes 39 | // Jeremy Jaussaud 40 | // Fabian Giesen 41 | // 42 | // Version history: 43 | // 44 | // 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section 45 | // 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles 46 | // 0.99 (2019-02-07) warning fixes 47 | // 0.11 (2017-03-03) return packing success/fail result 48 | // 0.10 (2016-10-25) remove cast-away-const to avoid warnings 49 | // 0.09 (2016-08-27) fix compiler warnings 50 | // 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) 51 | // 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) 52 | // 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort 53 | // 0.05: added STBRP_ASSERT to allow replacing assert 54 | // 0.04: fixed minor bug in STBRP_LARGE_RECTS support 55 | // 0.01: initial release 56 | // 57 | // LICENSE 58 | // 59 | // See end of file for license information. 60 | 61 | ////////////////////////////////////////////////////////////////////////////// 62 | // 63 | // INCLUDE SECTION 64 | // 65 | 66 | #ifndef STB_INCLUDE_STB_RECT_PACK_H 67 | #define STB_INCLUDE_STB_RECT_PACK_H 68 | 69 | #define STB_RECT_PACK_VERSION 1 70 | 71 | #ifdef STBRP_STATIC 72 | #define STBRP_DEF static 73 | #else 74 | #define STBRP_DEF extern 75 | #endif 76 | 77 | #ifdef __cplusplus 78 | extern "C" { 79 | #endif 80 | 81 | typedef struct stbrp_context stbrp_context; 82 | typedef struct stbrp_node stbrp_node; 83 | typedef struct stbrp_rect stbrp_rect; 84 | 85 | typedef int stbrp_coord; 86 | 87 | #define STBRP__MAXVAL 0x7fffffff 88 | // Mostly for internal use, but this is the maximum supported coordinate value. 89 | 90 | STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); 91 | // Assign packed locations to rectangles. The rectangles are of type 92 | // 'stbrp_rect' defined below, stored in the array 'rects', and there 93 | // are 'num_rects' many of them. 94 | // 95 | // Rectangles which are successfully packed have the 'was_packed' flag 96 | // set to a non-zero value and 'x' and 'y' store the minimum location 97 | // on each axis (i.e. bottom-left in cartesian coordinates, top-left 98 | // if you imagine y increasing downwards). Rectangles which do not fit 99 | // have the 'was_packed' flag set to 0. 100 | // 101 | // You should not try to access the 'rects' array from another thread 102 | // while this function is running, as the function temporarily reorders 103 | // the array while it executes. 104 | // 105 | // To pack into another rectangle, you need to call stbrp_init_target 106 | // again. To continue packing into the same rectangle, you can call 107 | // this function again. Calling this multiple times with multiple rect 108 | // arrays will probably produce worse packing results than calling it 109 | // a single time with the full rectangle array, but the option is 110 | // available. 111 | // 112 | // The function returns 1 if all of the rectangles were successfully 113 | // packed and 0 otherwise. 114 | 115 | struct stbrp_rect 116 | { 117 | // reserved for your use: 118 | int id; 119 | 120 | // input: 121 | stbrp_coord w, h; 122 | 123 | // output: 124 | stbrp_coord x, y; 125 | int was_packed; // non-zero if valid packing 126 | 127 | }; // 16 bytes, nominally 128 | 129 | 130 | STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); 131 | // Initialize a rectangle packer to: 132 | // pack a rectangle that is 'width' by 'height' in dimensions 133 | // using temporary storage provided by the array 'nodes', which is 'num_nodes' long 134 | // 135 | // You must call this function every time you start packing into a new target. 136 | // 137 | // There is no "shutdown" function. The 'nodes' memory must stay valid for 138 | // the following stbrp_pack_rects() call (or calls), but can be freed after 139 | // the call (or calls) finish. 140 | // 141 | // Note: to guarantee best results, either: 142 | // 1. make sure 'num_nodes' >= 'width' 143 | // or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' 144 | // 145 | // If you don't do either of the above things, widths will be quantized to multiples 146 | // of small integers to guarantee the algorithm doesn't run out of temporary storage. 147 | // 148 | // If you do #2, then the non-quantized algorithm will be used, but the algorithm 149 | // may run out of temporary storage and be unable to pack some rectangles. 150 | 151 | STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); 152 | // Optionally call this function after init but before doing any packing to 153 | // change the handling of the out-of-temp-memory scenario, described above. 154 | // If you call init again, this will be reset to the default (false). 155 | 156 | 157 | STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); 158 | // Optionally select which packing heuristic the library should use. Different 159 | // heuristics will produce better/worse results for different data sets. 160 | // If you call init again, this will be reset to the default. 161 | 162 | enum 163 | { 164 | STBRP_HEURISTIC_Skyline_default=0, 165 | STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, 166 | STBRP_HEURISTIC_Skyline_BF_sortHeight 167 | }; 168 | 169 | 170 | ////////////////////////////////////////////////////////////////////////////// 171 | // 172 | // the details of the following structures don't matter to you, but they must 173 | // be visible so you can handle the memory allocations for them 174 | 175 | struct stbrp_node 176 | { 177 | stbrp_coord x,y; 178 | stbrp_node *next; 179 | }; 180 | 181 | struct stbrp_context 182 | { 183 | int width; 184 | int height; 185 | int align; 186 | int init_mode; 187 | int heuristic; 188 | int num_nodes; 189 | stbrp_node *active_head; 190 | stbrp_node *free_head; 191 | stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' 192 | }; 193 | 194 | #ifdef __cplusplus 195 | } 196 | #endif 197 | 198 | #endif 199 | 200 | ////////////////////////////////////////////////////////////////////////////// 201 | // 202 | // IMPLEMENTATION SECTION 203 | // 204 | 205 | #ifdef STB_RECT_PACK_IMPLEMENTATION 206 | #ifndef STBRP_SORT 207 | #include 208 | #define STBRP_SORT qsort 209 | #endif 210 | 211 | #ifndef STBRP_ASSERT 212 | #include 213 | #define STBRP_ASSERT assert 214 | #endif 215 | 216 | #ifdef _MSC_VER 217 | #define STBRP__NOTUSED(v) (void)(v) 218 | #define STBRP__CDECL __cdecl 219 | #else 220 | #define STBRP__NOTUSED(v) (void)sizeof(v) 221 | #define STBRP__CDECL 222 | #endif 223 | 224 | enum 225 | { 226 | STBRP__INIT_skyline = 1 227 | }; 228 | 229 | STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) 230 | { 231 | switch (context->init_mode) { 232 | case STBRP__INIT_skyline: 233 | STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); 234 | context->heuristic = heuristic; 235 | break; 236 | default: 237 | STBRP_ASSERT(0); 238 | } 239 | } 240 | 241 | STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) 242 | { 243 | if (allow_out_of_mem) 244 | // if it's ok to run out of memory, then don't bother aligning them; 245 | // this gives better packing, but may fail due to OOM (even though 246 | // the rectangles easily fit). @TODO a smarter approach would be to only 247 | // quantize once we've hit OOM, then we could get rid of this parameter. 248 | context->align = 1; 249 | else { 250 | // if it's not ok to run out of memory, then quantize the widths 251 | // so that num_nodes is always enough nodes. 252 | // 253 | // I.e. num_nodes * align >= width 254 | // align >= width / num_nodes 255 | // align = ceil(width/num_nodes) 256 | 257 | context->align = (context->width + context->num_nodes-1) / context->num_nodes; 258 | } 259 | } 260 | 261 | STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) 262 | { 263 | int i; 264 | 265 | for (i=0; i < num_nodes-1; ++i) 266 | nodes[i].next = &nodes[i+1]; 267 | nodes[i].next = NULL; 268 | context->init_mode = STBRP__INIT_skyline; 269 | context->heuristic = STBRP_HEURISTIC_Skyline_default; 270 | context->free_head = &nodes[0]; 271 | context->active_head = &context->extra[0]; 272 | context->width = width; 273 | context->height = height; 274 | context->num_nodes = num_nodes; 275 | stbrp_setup_allow_out_of_mem(context, 0); 276 | 277 | // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) 278 | context->extra[0].x = 0; 279 | context->extra[0].y = 0; 280 | context->extra[0].next = &context->extra[1]; 281 | context->extra[1].x = (stbrp_coord) width; 282 | context->extra[1].y = (1<<30); 283 | context->extra[1].next = NULL; 284 | } 285 | 286 | // find minimum y position if it starts at x1 287 | static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste) 288 | { 289 | stbrp_node *node = first; 290 | int x1 = x0 + width; 291 | int min_y, visited_width, waste_area; 292 | 293 | STBRP__NOTUSED(c); 294 | 295 | STBRP_ASSERT(first->x <= x0); 296 | 297 | #if 0 298 | // skip in case we're past the node 299 | while (node->next->x <= x0) 300 | ++node; 301 | #else 302 | STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency 303 | #endif 304 | 305 | STBRP_ASSERT(node->x <= x0); 306 | 307 | min_y = 0; 308 | waste_area = 0; 309 | visited_width = 0; 310 | while (node->x < x1) { 311 | if (node->y > min_y) { 312 | // raise min_y higher. 313 | // we've accounted for all waste up to min_y, 314 | // but we'll now add more waste for everything we've visted 315 | waste_area += visited_width * (node->y - min_y); 316 | min_y = node->y; 317 | // the first time through, visited_width might be reduced 318 | if (node->x < x0) 319 | visited_width += node->next->x - x0; 320 | else 321 | visited_width += node->next->x - node->x; 322 | } else { 323 | // add waste area 324 | int under_width = node->next->x - node->x; 325 | if (under_width + visited_width > width) 326 | under_width = width - visited_width; 327 | waste_area += under_width * (min_y - node->y); 328 | visited_width += under_width; 329 | } 330 | node = node->next; 331 | } 332 | 333 | *pwaste = waste_area; 334 | return min_y; 335 | } 336 | 337 | typedef struct 338 | { 339 | int x,y; 340 | stbrp_node **prev_link; 341 | } stbrp__findresult; 342 | 343 | static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) 344 | { 345 | int best_waste = (1<<30), best_x, best_y = (1 << 30); 346 | stbrp__findresult fr; 347 | stbrp_node **prev, *node, *tail, **best = NULL; 348 | 349 | // align to multiple of c->align 350 | width = (width + c->align - 1); 351 | width -= width % c->align; 352 | STBRP_ASSERT(width % c->align == 0); 353 | 354 | // if it can't possibly fit, bail immediately 355 | if (width > c->width || height > c->height) { 356 | fr.prev_link = NULL; 357 | fr.x = fr.y = 0; 358 | return fr; 359 | } 360 | 361 | node = c->active_head; 362 | prev = &c->active_head; 363 | while (node->x + width <= c->width) { 364 | int y,waste; 365 | y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); 366 | if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL 367 | // bottom left 368 | if (y < best_y) { 369 | best_y = y; 370 | best = prev; 371 | } 372 | } else { 373 | // best-fit 374 | if (y + height <= c->height) { 375 | // can only use it if it first vertically 376 | if (y < best_y || (y == best_y && waste < best_waste)) { 377 | best_y = y; 378 | best_waste = waste; 379 | best = prev; 380 | } 381 | } 382 | } 383 | prev = &node->next; 384 | node = node->next; 385 | } 386 | 387 | best_x = (best == NULL) ? 0 : (*best)->x; 388 | 389 | // if doing best-fit (BF), we also have to try aligning right edge to each node position 390 | // 391 | // e.g, if fitting 392 | // 393 | // ____________________ 394 | // |____________________| 395 | // 396 | // into 397 | // 398 | // | | 399 | // | ____________| 400 | // |____________| 401 | // 402 | // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned 403 | // 404 | // This makes BF take about 2x the time 405 | 406 | if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { 407 | tail = c->active_head; 408 | node = c->active_head; 409 | prev = &c->active_head; 410 | // find first node that's admissible 411 | while (tail->x < width) 412 | tail = tail->next; 413 | while (tail) { 414 | int xpos = tail->x - width; 415 | int y,waste; 416 | STBRP_ASSERT(xpos >= 0); 417 | // find the left position that matches this 418 | while (node->next->x <= xpos) { 419 | prev = &node->next; 420 | node = node->next; 421 | } 422 | STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); 423 | y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); 424 | if (y + height <= c->height) { 425 | if (y <= best_y) { 426 | if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { 427 | best_x = xpos; 428 | STBRP_ASSERT(y <= best_y); 429 | best_y = y; 430 | best_waste = waste; 431 | best = prev; 432 | } 433 | } 434 | } 435 | tail = tail->next; 436 | } 437 | } 438 | 439 | fr.prev_link = best; 440 | fr.x = best_x; 441 | fr.y = best_y; 442 | return fr; 443 | } 444 | 445 | static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) 446 | { 447 | // find best position according to heuristic 448 | stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); 449 | stbrp_node *node, *cur; 450 | 451 | // bail if: 452 | // 1. it failed 453 | // 2. the best node doesn't fit (we don't always check this) 454 | // 3. we're out of memory 455 | if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { 456 | res.prev_link = NULL; 457 | return res; 458 | } 459 | 460 | // on success, create new node 461 | node = context->free_head; 462 | node->x = (stbrp_coord) res.x; 463 | node->y = (stbrp_coord) (res.y + height); 464 | 465 | context->free_head = node->next; 466 | 467 | // insert the new node into the right starting point, and 468 | // let 'cur' point to the remaining nodes needing to be 469 | // stiched back in 470 | 471 | cur = *res.prev_link; 472 | if (cur->x < res.x) { 473 | // preserve the existing one, so start testing with the next one 474 | stbrp_node *next = cur->next; 475 | cur->next = node; 476 | cur = next; 477 | } else { 478 | *res.prev_link = node; 479 | } 480 | 481 | // from here, traverse cur and free the nodes, until we get to one 482 | // that shouldn't be freed 483 | while (cur->next && cur->next->x <= res.x + width) { 484 | stbrp_node *next = cur->next; 485 | // move the current node to the free list 486 | cur->next = context->free_head; 487 | context->free_head = cur; 488 | cur = next; 489 | } 490 | 491 | // stitch the list back in 492 | node->next = cur; 493 | 494 | if (cur->x < res.x + width) 495 | cur->x = (stbrp_coord) (res.x + width); 496 | 497 | #ifdef _DEBUG 498 | cur = context->active_head; 499 | while (cur->x < context->width) { 500 | STBRP_ASSERT(cur->x < cur->next->x); 501 | cur = cur->next; 502 | } 503 | STBRP_ASSERT(cur->next == NULL); 504 | 505 | { 506 | int count=0; 507 | cur = context->active_head; 508 | while (cur) { 509 | cur = cur->next; 510 | ++count; 511 | } 512 | cur = context->free_head; 513 | while (cur) { 514 | cur = cur->next; 515 | ++count; 516 | } 517 | STBRP_ASSERT(count == context->num_nodes+2); 518 | } 519 | #endif 520 | 521 | return res; 522 | } 523 | 524 | static int STBRP__CDECL rect_height_compare(const void *a, const void *b) 525 | { 526 | const stbrp_rect *p = (const stbrp_rect *) a; 527 | const stbrp_rect *q = (const stbrp_rect *) b; 528 | if (p->h > q->h) 529 | return -1; 530 | if (p->h < q->h) 531 | return 1; 532 | return (p->w > q->w) ? -1 : (p->w < q->w); 533 | } 534 | 535 | static int STBRP__CDECL rect_original_order(const void *a, const void *b) 536 | { 537 | const stbrp_rect *p = (const stbrp_rect *) a; 538 | const stbrp_rect *q = (const stbrp_rect *) b; 539 | return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); 540 | } 541 | 542 | STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) 543 | { 544 | int i, all_rects_packed = 1; 545 | 546 | // we use the 'was_packed' field internally to allow sorting/unsorting 547 | for (i=0; i < num_rects; ++i) { 548 | rects[i].was_packed = i; 549 | } 550 | 551 | // sort according to heuristic 552 | STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); 553 | 554 | for (i=0; i < num_rects; ++i) { 555 | if (rects[i].w == 0 || rects[i].h == 0) { 556 | rects[i].x = rects[i].y = 0; // empty rect needs no space 557 | } else { 558 | stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); 559 | if (fr.prev_link) { 560 | rects[i].x = (stbrp_coord) fr.x; 561 | rects[i].y = (stbrp_coord) fr.y; 562 | } else { 563 | rects[i].x = rects[i].y = STBRP__MAXVAL; 564 | } 565 | } 566 | } 567 | 568 | // unsort 569 | STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); 570 | 571 | // set was_packed flags and all_rects_packed status 572 | for (i=0; i < num_rects; ++i) { 573 | rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); 574 | if (!rects[i].was_packed) 575 | all_rects_packed = 0; 576 | } 577 | 578 | // return the all_rects_packed status 579 | return all_rects_packed; 580 | } 581 | #endif 582 | 583 | /* 584 | ------------------------------------------------------------------------------ 585 | This software is available under 2 licenses -- choose whichever you prefer. 586 | ------------------------------------------------------------------------------ 587 | ALTERNATIVE A - MIT License 588 | Copyright (c) 2017 Sean Barrett 589 | Permission is hereby granted, free of charge, to any person obtaining a copy of 590 | this software and associated documentation files (the "Software"), to deal in 591 | the Software without restriction, including without limitation the rights to 592 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 593 | of the Software, and to permit persons to whom the Software is furnished to do 594 | so, subject to the following conditions: 595 | The above copyright notice and this permission notice shall be included in all 596 | copies or substantial portions of the Software. 597 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 598 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 599 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 600 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 601 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 602 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 603 | SOFTWARE. 604 | ------------------------------------------------------------------------------ 605 | ALTERNATIVE B - Public Domain (www.unlicense.org) 606 | This is free and unencumbered software released into the public domain. 607 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 608 | software, either in source code form or as a compiled binary, for any purpose, 609 | commercial or non-commercial, and by any means. 610 | In jurisdictions that recognize copyright laws, the author or authors of this 611 | software dedicate any and all copyright interest in the software to the public 612 | domain. We make this dedication for the benefit of the public at large and to 613 | the detriment of our heirs and successors. We intend this dedication to be an 614 | overt act of relinquishment in perpetuity of all present and future rights to 615 | this software under copyright law. 616 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 617 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 618 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 619 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 620 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 621 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 622 | ------------------------------------------------------------------------------ 623 | */ 624 | -------------------------------------------------------------------------------- /Demo/VgerDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 29AFA89927B56FE700BCA9AF /* Triangle.svg in Resources */ = {isa = PBXBuildFile; fileRef = 29AFA89827B56FE700BCA9AF /* Triangle.svg */; }; 11 | 29AFA89A27B56FE700BCA9AF /* Triangle.svg in Resources */ = {isa = PBXBuildFile; fileRef = 29AFA89827B56FE700BCA9AF /* Triangle.svg */; }; 12 | 29B08BFE27D71CAC00E0AB8F /* HelloView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B08BFD27D71CAC00E0AB8F /* HelloView.swift */; }; 13 | 29B08BFF27D71CAC00E0AB8F /* HelloView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B08BFD27D71CAC00E0AB8F /* HelloView.swift */; }; 14 | 29E7DA2926509E5800A196FF /* TigerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29E7DA2826509E5800A196FF /* TigerView.swift */; }; 15 | 29E7DA2A26509E5800A196FF /* TigerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29E7DA2826509E5800A196FF /* TigerView.swift */; }; 16 | 29E7DA2F26509EA400A196FF /* nanosvg.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E7DA2E26509EA400A196FF /* nanosvg.m */; }; 17 | 29E7DA3026509EA400A196FF /* nanosvg.m in Sources */ = {isa = PBXBuildFile; fileRef = 29E7DA2E26509EA400A196FF /* nanosvg.m */; }; 18 | 29E7DA392650B6D600A196FF /* Ghostscript_Tiger.svg in Resources */ = {isa = PBXBuildFile; fileRef = 29E7DA382650B6D600A196FF /* Ghostscript_Tiger.svg */; }; 19 | 29E7DA3A2650B6D600A196FF /* Ghostscript_Tiger.svg in Resources */ = {isa = PBXBuildFile; fileRef = 29E7DA382650B6D600A196FF /* Ghostscript_Tiger.svg */; }; 20 | 29F372C726421D7F00B38055 /* VgerDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F372B426421D7E00B38055 /* VgerDemoApp.swift */; }; 21 | 29F372C826421D7F00B38055 /* VgerDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F372B426421D7E00B38055 /* VgerDemoApp.swift */; }; 22 | 29F372C926421D7F00B38055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F372B526421D7E00B38055 /* ContentView.swift */; }; 23 | 29F372CA26421D7F00B38055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F372B526421D7E00B38055 /* ContentView.swift */; }; 24 | 29F372CB26421D7F00B38055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 29F372B626421D7F00B38055 /* Assets.xcassets */; }; 25 | 29F372CC26421D7F00B38055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 29F372B626421D7F00B38055 /* Assets.xcassets */; }; 26 | 29F372E5264223DE00B38055 /* DemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F372E4264223DE00B38055 /* DemoView.swift */; }; 27 | 29F372E6264223DE00B38055 /* DemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F372E4264223DE00B38055 /* DemoView.swift */; }; 28 | F1AD950028C5793200593D4F /* vger in Frameworks */ = {isa = PBXBuildFile; productRef = F1AD94FF28C5793200593D4F /* vger */; }; 29 | F1AD950228C5793500593D4F /* vger in Frameworks */ = {isa = PBXBuildFile; productRef = F1AD950128C5793500593D4F /* vger */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 29AFA89827B56FE700BCA9AF /* Triangle.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = Triangle.svg; sourceTree = ""; }; 34 | 29B08BFD27D71CAC00E0AB8F /* HelloView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelloView.swift; sourceTree = ""; }; 35 | 29E7DA2826509E5800A196FF /* TigerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TigerView.swift; sourceTree = ""; }; 36 | 29E7DA2C26509EA300A196FF /* VgerDemo (iOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VgerDemo (iOS)-Bridging-Header.h"; sourceTree = ""; }; 37 | 29E7DA2D26509EA300A196FF /* VgerDemo (macOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VgerDemo (macOS)-Bridging-Header.h"; sourceTree = ""; }; 38 | 29E7DA2E26509EA400A196FF /* nanosvg.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = nanosvg.m; sourceTree = ""; }; 39 | 29E7DA382650B6D600A196FF /* Ghostscript_Tiger.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = Ghostscript_Tiger.svg; sourceTree = ""; }; 40 | 29E7DA3C2650BBA200A196FF /* nanosvg.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = nanosvg.h; sourceTree = ""; }; 41 | 29F372B426421D7E00B38055 /* VgerDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VgerDemoApp.swift; sourceTree = ""; }; 42 | 29F372B526421D7E00B38055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 43 | 29F372B626421D7F00B38055 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 44 | 29F372BB26421D7F00B38055 /* VgerDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VgerDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 29F372BE26421D7F00B38055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 46 | 29F372C326421D7F00B38055 /* VgerDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VgerDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 29F372C526421D7F00B38055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 29F372C626421D7F00B38055 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; 49 | 29F372E4264223DE00B38055 /* DemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoView.swift; sourceTree = ""; }; 50 | F1AD94FE28C578D700593D4F /* vger */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = vger; path = ..; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 29F372B826421D7F00B38055 /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | F1AD950028C5793200593D4F /* vger in Frameworks */, 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | 29F372C026421D7F00B38055 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | F1AD950228C5793500593D4F /* vger in Frameworks */, 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | /* End PBXFrameworksBuildPhase section */ 71 | 72 | /* Begin PBXGroup section */ 73 | 29F372AE26421D7E00B38055 = { 74 | isa = PBXGroup; 75 | children = ( 76 | F1AD94FD28C578D700593D4F /* Packages */, 77 | 29F372B326421D7E00B38055 /* Shared */, 78 | 29F372BD26421D7F00B38055 /* iOS */, 79 | 29F372C426421D7F00B38055 /* macOS */, 80 | 29F372BC26421D7F00B38055 /* Products */, 81 | 29F372E0264222BE00B38055 /* Frameworks */, 82 | ); 83 | sourceTree = ""; 84 | }; 85 | 29F372B326421D7E00B38055 /* Shared */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 29F372B426421D7E00B38055 /* VgerDemoApp.swift */, 89 | 29F372B526421D7E00B38055 /* ContentView.swift */, 90 | 29F372B626421D7F00B38055 /* Assets.xcassets */, 91 | 29F372E4264223DE00B38055 /* DemoView.swift */, 92 | 29E7DA2826509E5800A196FF /* TigerView.swift */, 93 | 29B08BFD27D71CAC00E0AB8F /* HelloView.swift */, 94 | 29E7DA3C2650BBA200A196FF /* nanosvg.h */, 95 | 29E7DA2E26509EA400A196FF /* nanosvg.m */, 96 | 29E7DA2C26509EA300A196FF /* VgerDemo (iOS)-Bridging-Header.h */, 97 | 29E7DA2D26509EA300A196FF /* VgerDemo (macOS)-Bridging-Header.h */, 98 | 29E7DA382650B6D600A196FF /* Ghostscript_Tiger.svg */, 99 | 29AFA89827B56FE700BCA9AF /* Triangle.svg */, 100 | ); 101 | path = Shared; 102 | sourceTree = ""; 103 | }; 104 | 29F372BC26421D7F00B38055 /* Products */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 29F372BB26421D7F00B38055 /* VgerDemo.app */, 108 | 29F372C326421D7F00B38055 /* VgerDemo.app */, 109 | ); 110 | name = Products; 111 | sourceTree = ""; 112 | }; 113 | 29F372BD26421D7F00B38055 /* iOS */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 29F372BE26421D7F00B38055 /* Info.plist */, 117 | ); 118 | path = iOS; 119 | sourceTree = ""; 120 | }; 121 | 29F372C426421D7F00B38055 /* macOS */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 29F372C526421D7F00B38055 /* Info.plist */, 125 | 29F372C626421D7F00B38055 /* macOS.entitlements */, 126 | ); 127 | path = macOS; 128 | sourceTree = ""; 129 | }; 130 | 29F372E0264222BE00B38055 /* Frameworks */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | ); 134 | name = Frameworks; 135 | sourceTree = ""; 136 | }; 137 | F1AD94FD28C578D700593D4F /* Packages */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | F1AD94FE28C578D700593D4F /* vger */, 141 | ); 142 | name = Packages; 143 | sourceTree = ""; 144 | }; 145 | /* End PBXGroup section */ 146 | 147 | /* Begin PBXNativeTarget section */ 148 | 29F372BA26421D7F00B38055 /* VgerDemo (iOS) */ = { 149 | isa = PBXNativeTarget; 150 | buildConfigurationList = 29F372CF26421D7F00B38055 /* Build configuration list for PBXNativeTarget "VgerDemo (iOS)" */; 151 | buildPhases = ( 152 | 29F372B726421D7F00B38055 /* Sources */, 153 | 29F372B826421D7F00B38055 /* Frameworks */, 154 | 29F372B926421D7F00B38055 /* Resources */, 155 | ); 156 | buildRules = ( 157 | ); 158 | dependencies = ( 159 | ); 160 | name = "VgerDemo (iOS)"; 161 | packageProductDependencies = ( 162 | F1AD94FF28C5793200593D4F /* vger */, 163 | ); 164 | productName = "VgerDemo (iOS)"; 165 | productReference = 29F372BB26421D7F00B38055 /* VgerDemo.app */; 166 | productType = "com.apple.product-type.application"; 167 | }; 168 | 29F372C226421D7F00B38055 /* VgerDemo (macOS) */ = { 169 | isa = PBXNativeTarget; 170 | buildConfigurationList = 29F372D226421D7F00B38055 /* Build configuration list for PBXNativeTarget "VgerDemo (macOS)" */; 171 | buildPhases = ( 172 | 29F372BF26421D7F00B38055 /* Sources */, 173 | 29F372C026421D7F00B38055 /* Frameworks */, 174 | 29F372C126421D7F00B38055 /* Resources */, 175 | ); 176 | buildRules = ( 177 | ); 178 | dependencies = ( 179 | ); 180 | name = "VgerDemo (macOS)"; 181 | packageProductDependencies = ( 182 | F1AD950128C5793500593D4F /* vger */, 183 | ); 184 | productName = "VgerDemo (macOS)"; 185 | productReference = 29F372C326421D7F00B38055 /* VgerDemo.app */; 186 | productType = "com.apple.product-type.application"; 187 | }; 188 | /* End PBXNativeTarget section */ 189 | 190 | /* Begin PBXProject section */ 191 | 29F372AF26421D7E00B38055 /* Project object */ = { 192 | isa = PBXProject; 193 | attributes = { 194 | LastSwiftUpdateCheck = 1250; 195 | LastUpgradeCheck = 1340; 196 | TargetAttributes = { 197 | 29F372BA26421D7F00B38055 = { 198 | CreatedOnToolsVersion = 12.5; 199 | LastSwiftMigration = 1250; 200 | }; 201 | 29F372C226421D7F00B38055 = { 202 | CreatedOnToolsVersion = 12.5; 203 | LastSwiftMigration = 1250; 204 | }; 205 | }; 206 | }; 207 | buildConfigurationList = 29F372B226421D7E00B38055 /* Build configuration list for PBXProject "VgerDemo" */; 208 | compatibilityVersion = "Xcode 9.3"; 209 | developmentRegion = en; 210 | hasScannedForEncodings = 0; 211 | knownRegions = ( 212 | en, 213 | Base, 214 | ); 215 | mainGroup = 29F372AE26421D7E00B38055; 216 | packageReferences = ( 217 | ); 218 | productRefGroup = 29F372BC26421D7F00B38055 /* Products */; 219 | projectDirPath = ""; 220 | projectRoot = ""; 221 | targets = ( 222 | 29F372BA26421D7F00B38055 /* VgerDemo (iOS) */, 223 | 29F372C226421D7F00B38055 /* VgerDemo (macOS) */, 224 | ); 225 | }; 226 | /* End PBXProject section */ 227 | 228 | /* Begin PBXResourcesBuildPhase section */ 229 | 29F372B926421D7F00B38055 /* Resources */ = { 230 | isa = PBXResourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | 29AFA89927B56FE700BCA9AF /* Triangle.svg in Resources */, 234 | 29F372CB26421D7F00B38055 /* Assets.xcassets in Resources */, 235 | 29E7DA392650B6D600A196FF /* Ghostscript_Tiger.svg in Resources */, 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | 29F372C126421D7F00B38055 /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | 29AFA89A27B56FE700BCA9AF /* Triangle.svg in Resources */, 244 | 29F372CC26421D7F00B38055 /* Assets.xcassets in Resources */, 245 | 29E7DA3A2650B6D600A196FF /* Ghostscript_Tiger.svg in Resources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXResourcesBuildPhase section */ 250 | 251 | /* Begin PBXSourcesBuildPhase section */ 252 | 29F372B726421D7F00B38055 /* Sources */ = { 253 | isa = PBXSourcesBuildPhase; 254 | buildActionMask = 2147483647; 255 | files = ( 256 | 29E7DA2926509E5800A196FF /* TigerView.swift in Sources */, 257 | 29F372C926421D7F00B38055 /* ContentView.swift in Sources */, 258 | 29F372E5264223DE00B38055 /* DemoView.swift in Sources */, 259 | 29B08BFE27D71CAC00E0AB8F /* HelloView.swift in Sources */, 260 | 29F372C726421D7F00B38055 /* VgerDemoApp.swift in Sources */, 261 | 29E7DA2F26509EA400A196FF /* nanosvg.m in Sources */, 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | }; 265 | 29F372BF26421D7F00B38055 /* Sources */ = { 266 | isa = PBXSourcesBuildPhase; 267 | buildActionMask = 2147483647; 268 | files = ( 269 | 29E7DA2A26509E5800A196FF /* TigerView.swift in Sources */, 270 | 29F372CA26421D7F00B38055 /* ContentView.swift in Sources */, 271 | 29F372E6264223DE00B38055 /* DemoView.swift in Sources */, 272 | 29B08BFF27D71CAC00E0AB8F /* HelloView.swift in Sources */, 273 | 29F372C826421D7F00B38055 /* VgerDemoApp.swift in Sources */, 274 | 29E7DA3026509EA400A196FF /* nanosvg.m in Sources */, 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | /* End PBXSourcesBuildPhase section */ 279 | 280 | /* Begin XCBuildConfiguration section */ 281 | 29F372CD26421D7F00B38055 /* Debug */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ALWAYS_SEARCH_USER_PATHS = NO; 285 | CLANG_ANALYZER_NONNULL = YES; 286 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 288 | CLANG_CXX_LIBRARY = "libc++"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_ENABLE_OBJC_WEAK = YES; 292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_COMMA = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 299 | CLANG_WARN_EMPTY_BODY = YES; 300 | CLANG_WARN_ENUM_CONVERSION = YES; 301 | CLANG_WARN_INFINITE_RECURSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 307 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 308 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 309 | CLANG_WARN_STRICT_PROTOTYPES = YES; 310 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 311 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = dwarf; 316 | ENABLE_STRICT_OBJC_MSGSEND = YES; 317 | ENABLE_TESTABILITY = YES; 318 | GCC_C_LANGUAGE_STANDARD = gnu11; 319 | GCC_DYNAMIC_NO_PIC = NO; 320 | GCC_NO_COMMON_BLOCKS = YES; 321 | GCC_OPTIMIZATION_LEVEL = 0; 322 | GCC_PREPROCESSOR_DEFINITIONS = ( 323 | "DEBUG=1", 324 | "$(inherited)", 325 | ); 326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 328 | GCC_WARN_UNDECLARED_SELECTOR = YES; 329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 330 | GCC_WARN_UNUSED_FUNCTION = YES; 331 | GCC_WARN_UNUSED_VARIABLE = YES; 332 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 333 | MTL_FAST_MATH = YES; 334 | ONLY_ACTIVE_ARCH = YES; 335 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 336 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 337 | }; 338 | name = Debug; 339 | }; 340 | 29F372CE26421D7F00B38055 /* Release */ = { 341 | isa = XCBuildConfiguration; 342 | buildSettings = { 343 | ALWAYS_SEARCH_USER_PATHS = NO; 344 | CLANG_ANALYZER_NONNULL = YES; 345 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 346 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 347 | CLANG_CXX_LIBRARY = "libc++"; 348 | CLANG_ENABLE_MODULES = YES; 349 | CLANG_ENABLE_OBJC_ARC = YES; 350 | CLANG_ENABLE_OBJC_WEAK = YES; 351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_COMMA = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 357 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INFINITE_RECURSION = YES; 361 | CLANG_WARN_INT_CONVERSION = YES; 362 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 364 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 366 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 367 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 368 | CLANG_WARN_STRICT_PROTOTYPES = YES; 369 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 370 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 371 | CLANG_WARN_UNREACHABLE_CODE = YES; 372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 373 | COPY_PHASE_STRIP = NO; 374 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 375 | ENABLE_NS_ASSERTIONS = NO; 376 | ENABLE_STRICT_OBJC_MSGSEND = YES; 377 | GCC_C_LANGUAGE_STANDARD = gnu11; 378 | GCC_NO_COMMON_BLOCKS = YES; 379 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 380 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 381 | GCC_WARN_UNDECLARED_SELECTOR = YES; 382 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 383 | GCC_WARN_UNUSED_FUNCTION = YES; 384 | GCC_WARN_UNUSED_VARIABLE = YES; 385 | MTL_ENABLE_DEBUG_INFO = NO; 386 | MTL_FAST_MATH = YES; 387 | SWIFT_COMPILATION_MODE = wholemodule; 388 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 389 | }; 390 | name = Release; 391 | }; 392 | 29F372D026421D7F00B38055 /* Debug */ = { 393 | isa = XCBuildConfiguration; 394 | buildSettings = { 395 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 396 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 397 | CLANG_ENABLE_MODULES = YES; 398 | CODE_SIGN_STYLE = Automatic; 399 | DEVELOPMENT_TEAM = J6F2XLS3BY; 400 | ENABLE_PREVIEWS = YES; 401 | INFOPLIST_FILE = iOS/Info.plist; 402 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 403 | LD_RUNPATH_SEARCH_PATHS = ( 404 | "$(inherited)", 405 | "@executable_path/Frameworks", 406 | ); 407 | PRODUCT_BUNDLE_IDENTIFIER = com.audulus.VgerDemo; 408 | PRODUCT_NAME = VgerDemo; 409 | SDKROOT = iphoneos; 410 | SWIFT_OBJC_BRIDGING_HEADER = "Shared/VgerDemo (iOS)-Bridging-Header.h"; 411 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 412 | SWIFT_VERSION = 5.0; 413 | TARGETED_DEVICE_FAMILY = "1,2"; 414 | }; 415 | name = Debug; 416 | }; 417 | 29F372D126421D7F00B38055 /* Release */ = { 418 | isa = XCBuildConfiguration; 419 | buildSettings = { 420 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 421 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 422 | CLANG_ENABLE_MODULES = YES; 423 | CODE_SIGN_STYLE = Automatic; 424 | DEVELOPMENT_TEAM = J6F2XLS3BY; 425 | ENABLE_PREVIEWS = YES; 426 | INFOPLIST_FILE = iOS/Info.plist; 427 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 428 | LD_RUNPATH_SEARCH_PATHS = ( 429 | "$(inherited)", 430 | "@executable_path/Frameworks", 431 | ); 432 | PRODUCT_BUNDLE_IDENTIFIER = com.audulus.VgerDemo; 433 | PRODUCT_NAME = VgerDemo; 434 | SDKROOT = iphoneos; 435 | SWIFT_OBJC_BRIDGING_HEADER = "Shared/VgerDemo (iOS)-Bridging-Header.h"; 436 | SWIFT_VERSION = 5.0; 437 | TARGETED_DEVICE_FAMILY = "1,2"; 438 | VALIDATE_PRODUCT = YES; 439 | }; 440 | name = Release; 441 | }; 442 | 29F372D326421D7F00B38055 /* Debug */ = { 443 | isa = XCBuildConfiguration; 444 | buildSettings = { 445 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 446 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 447 | CLANG_ENABLE_MODULES = YES; 448 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; 449 | CODE_SIGN_IDENTITY = "-"; 450 | CODE_SIGN_STYLE = Automatic; 451 | COMBINE_HIDPI_IMAGES = YES; 452 | ENABLE_PREVIEWS = YES; 453 | INFOPLIST_FILE = macOS/Info.plist; 454 | LD_RUNPATH_SEARCH_PATHS = ( 455 | "$(inherited)", 456 | "@executable_path/../Frameworks", 457 | ); 458 | MACOSX_DEPLOYMENT_TARGET = 11.0; 459 | PRODUCT_BUNDLE_IDENTIFIER = com.audulus.VgerDemo; 460 | PRODUCT_NAME = VgerDemo; 461 | SDKROOT = macosx; 462 | SWIFT_OBJC_BRIDGING_HEADER = "Shared/VgerDemo (macOS)-Bridging-Header.h"; 463 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 464 | SWIFT_VERSION = 5.0; 465 | }; 466 | name = Debug; 467 | }; 468 | 29F372D426421D7F00B38055 /* Release */ = { 469 | isa = XCBuildConfiguration; 470 | buildSettings = { 471 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 472 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 473 | CLANG_ENABLE_MODULES = YES; 474 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; 475 | CODE_SIGN_IDENTITY = "-"; 476 | CODE_SIGN_STYLE = Automatic; 477 | COMBINE_HIDPI_IMAGES = YES; 478 | ENABLE_PREVIEWS = YES; 479 | INFOPLIST_FILE = macOS/Info.plist; 480 | LD_RUNPATH_SEARCH_PATHS = ( 481 | "$(inherited)", 482 | "@executable_path/../Frameworks", 483 | ); 484 | MACOSX_DEPLOYMENT_TARGET = 11.0; 485 | PRODUCT_BUNDLE_IDENTIFIER = com.audulus.VgerDemo; 486 | PRODUCT_NAME = VgerDemo; 487 | SDKROOT = macosx; 488 | SWIFT_OBJC_BRIDGING_HEADER = "Shared/VgerDemo (macOS)-Bridging-Header.h"; 489 | SWIFT_VERSION = 5.0; 490 | }; 491 | name = Release; 492 | }; 493 | /* End XCBuildConfiguration section */ 494 | 495 | /* Begin XCConfigurationList section */ 496 | 29F372B226421D7E00B38055 /* Build configuration list for PBXProject "VgerDemo" */ = { 497 | isa = XCConfigurationList; 498 | buildConfigurations = ( 499 | 29F372CD26421D7F00B38055 /* Debug */, 500 | 29F372CE26421D7F00B38055 /* Release */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | 29F372CF26421D7F00B38055 /* Build configuration list for PBXNativeTarget "VgerDemo (iOS)" */ = { 506 | isa = XCConfigurationList; 507 | buildConfigurations = ( 508 | 29F372D026421D7F00B38055 /* Debug */, 509 | 29F372D126421D7F00B38055 /* Release */, 510 | ); 511 | defaultConfigurationIsVisible = 0; 512 | defaultConfigurationName = Release; 513 | }; 514 | 29F372D226421D7F00B38055 /* Build configuration list for PBXNativeTarget "VgerDemo (macOS)" */ = { 515 | isa = XCConfigurationList; 516 | buildConfigurations = ( 517 | 29F372D326421D7F00B38055 /* Debug */, 518 | 29F372D426421D7F00B38055 /* Release */, 519 | ); 520 | defaultConfigurationIsVisible = 0; 521 | defaultConfigurationName = Release; 522 | }; 523 | /* End XCConfigurationList section */ 524 | 525 | /* Begin XCSwiftPackageProductDependency section */ 526 | F1AD94FF28C5793200593D4F /* vger */ = { 527 | isa = XCSwiftPackageProductDependency; 528 | productName = vger; 529 | }; 530 | F1AD950128C5793500593D4F /* vger */ = { 531 | isa = XCSwiftPackageProductDependency; 532 | productName = vger; 533 | }; 534 | /* End XCSwiftPackageProductDependency section */ 535 | }; 536 | rootObject = 29F372AF26421D7E00B38055 /* Project object */; 537 | } 538 | -------------------------------------------------------------------------------- /Tests/vgerTests/vgerTests.mm: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Audulus LLC. All rights reserved. 2 | 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import "../../Sources/vger/vgerRenderer.h" 8 | #import "testUtils.h" 9 | #import "vger.h" 10 | #include "nanovg_mtl.h" 11 | #include 12 | 13 | #define NANOSVG_IMPLEMENTATION 14 | #include "nanosvg.h" 15 | 16 | #import "../../Sources/vger/sdf.h" 17 | #import "../../Sources/vger/vger_private.h" 18 | 19 | using namespace simd; 20 | 21 | @interface vgerTests : XCTestCase { 22 | id device; 23 | id queue; 24 | id texture; 25 | MTLRenderPassDescriptor* pass; 26 | MTKTextureLoader* loader; 27 | } 28 | 29 | @end 30 | 31 | @implementation vgerTests 32 | 33 | - (void)setUp { 34 | device = MTLCreateSystemDefaultDevice(); 35 | queue = [device newCommandQueue]; 36 | loader = [[MTKTextureLoader alloc] initWithDevice: device]; 37 | 38 | int w = 512; 39 | int h = 512; 40 | 41 | auto textureDesc = [MTLTextureDescriptor 42 | texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm 43 | width:w 44 | height:h 45 | mipmapped:NO]; 46 | 47 | textureDesc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; 48 | textureDesc.storageMode = MTLStorageModeShared; 49 | 50 | texture = [device newTextureWithDescriptor:textureDesc]; 51 | assert(texture); 52 | 53 | pass = [MTLRenderPassDescriptor new]; 54 | pass.colorAttachments[0].texture = texture; 55 | pass.colorAttachments[0].storeAction = MTLStoreActionStore; 56 | pass.colorAttachments[0].loadAction = MTLLoadActionClear; 57 | pass.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 1); 58 | } 59 | 60 | - (void)tearDown { 61 | // Put teardown code here. This method is called after the invocation of each test method in the class. 62 | } 63 | 64 | - (void) render:(vgerContext)vg name:(NSString*) name { 65 | 66 | auto commandBuffer = [queue commandBuffer]; 67 | 68 | vgerEncode(vg, commandBuffer, pass); 69 | 70 | // Sync texture on macOS 71 | #if TARGET_OS_OSX 72 | auto blitEncoder = [commandBuffer blitCommandEncoder]; 73 | [blitEncoder synchronizeResource:texture]; 74 | [blitEncoder endEncoding]; 75 | #endif 76 | 77 | [commandBuffer commit]; 78 | [commandBuffer waitUntilCompleted]; 79 | 80 | showTexture(texture, name); 81 | 82 | } 83 | 84 | - (void) checkRender:(vgerContext)vg name:(NSString*) name { 85 | 86 | auto commandBuffer = [queue commandBuffer]; 87 | 88 | vgerEncode(vg, commandBuffer, pass); 89 | 90 | // Sync texture on macOS 91 | #if TARGET_OS_OSX 92 | auto blitEncoder = [commandBuffer blitCommandEncoder]; 93 | [blitEncoder synchronizeResource:texture]; 94 | [blitEncoder endEncoding]; 95 | #endif 96 | 97 | [commandBuffer commit]; 98 | [commandBuffer waitUntilCompleted]; 99 | 100 | XCTAssertTrue(checkTexture(texture, name)); 101 | 102 | } 103 | 104 | static void SplitBezier(float t, 105 | simd_float2 cv[3], 106 | simd_float2 a[3], 107 | simd_float2 b[3]) { 108 | 109 | a[0] = cv[0]; 110 | b[2] = cv[2]; 111 | 112 | a[1] = simd_mix(cv[0], cv[1], t); 113 | b[1] = simd_mix(cv[1], cv[2], t); 114 | 115 | a[2] = b[0] = simd_mix(a[1], b[1], t); 116 | 117 | } 118 | 119 | - (void) testSizes { 120 | XCTAssertEqual(sizeof(vgerPaint), 128); 121 | XCTAssertEqual(sizeof(vgerPrim), 96); 122 | } 123 | 124 | - (void) testBasic { 125 | 126 | float theta = 0; 127 | float ap = .5 * M_PI; 128 | 129 | auto vg = vgerNew(0, MTLPixelFormatBGRA8Unorm); 130 | 131 | vgerBegin(vg, 512, 512, 1.0); 132 | 133 | auto white = vgerColorPaint(vg, float4{1,1,1,1}); 134 | auto cyan = vgerColorPaint(vg, float4{0,1,1,1}); 135 | auto magenta = vgerColorPaint(vg, float4{1,0,1,1}); 136 | auto red = vgerColorPaint(vg, float4{1,0,0,1}); 137 | 138 | vgerFillCircle(vg, float2{256, 256}, 40, cyan); 139 | vgerStrokeBezier(vg, {{256,256}, {256,384}, {384,384}}, 1, white); 140 | vgerStrokeBezier(vg, {{384,384}, {512,512}, {512,512}}, 1, red); // Degenerate 141 | vgerFillRect(vg, float2{400,100}, float2{450,150}, 10, vgerLinearGradient(vg, float2{400,100}, float2{450, 150}, float4{0,1,1,1}, float4{1,0,1,1}, 0)); 142 | vgerFillRect(vg, float2{400,200}, float2{450,250}, 10, vgerRadialGradient(vg, float2{425,225}, 10, 40, float4{0,1,1,1}, float4{1,0,1,1}, 0)); 143 | vgerStrokeArc(vg, float2{100,400}, 30, 3, theta, ap, white); 144 | vgerStrokeSegment(vg, float2{100,100}, float2{200,200}, 10, magenta); 145 | vgerStrokeRect(vg, float2{400,100}, float2{450,150}, 10, 2.0, magenta); 146 | vgerStrokeWire(vg, float2{200,100}, float2{300,200}, 3, white); 147 | 148 | // Basic stroke test. 149 | vgerSave(vg); 150 | vgerScale(vg, float2{100, 100}); 151 | float2 cvs2[] = {0, {1,0}, {1,1}, {0,1}, {0, 2}, {0,1} }; 152 | vgerMoveTo(vg, cvs2[0]); 153 | vgerQuadTo(vg, cvs2[1], cvs2[2]); 154 | vgerQuadTo(vg, cvs2[3], cvs2[4]); 155 | vgerQuadTo(vg, cvs2[5], cvs2[0]); 156 | vgerFill(vg, white); 157 | vgerRestore(vg); 158 | 159 | // Stroke test with quadratic bezier with start and end 160 | // cvs at the same y. 161 | vgerSave(vg); 162 | vgerTranslate(vg, float2{20, 200}); 163 | vgerScale(vg, float2{100, 100}); 164 | vgerMoveTo(vg, float2{0,0}); 165 | vgerLineTo(vg, float2{1,0}); 166 | vgerQuadTo(vg, float2{.5, .5}, float2{0,0}); 167 | vgerFill(vg, white); 168 | vgerRestore(vg); 169 | 170 | [self checkRender:vg name:@"vger_basics.png"]; 171 | 172 | vgerDelete(vg); 173 | 174 | } 175 | 176 | - (void) testTransformStack { 177 | 178 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 179 | 180 | vgerBegin(vger, 512, 512, 1.0); 181 | 182 | XCTAssertTrue(equal(vgerTransform(vger, float2{0,0}), float2{0, 0})); 183 | 184 | auto cyan = vgerColorPaint(vger, float4{0,1,1,1}); 185 | vgerFillCircle(vger, float2{20,20}, 10, cyan); 186 | 187 | vgerSave(vger); 188 | vgerTranslate(vger, float2{100,0.0f}); 189 | vgerTranslate(vger, float2{0.0f,100}); 190 | vgerScale(vger, float2{4.0f, 4.0f}); 191 | vgerFillCircle(vger, float2{20,20}, 10, cyan); 192 | 193 | vgerRestore(vger); 194 | 195 | [self checkRender:vger name:@"xform.png"]; 196 | 197 | vgerDelete(vger); 198 | } 199 | 200 | - (void) testRotate { 201 | 202 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 203 | 204 | vgerBegin(vger, 512, 512, 1.0); 205 | 206 | XCTAssertTrue(equal(vgerTransform(vger, float2{0,0}), float2{0, 0})); 207 | 208 | auto cyan = vgerColorPaint(vger, float4{0,1,1,1}); 209 | 210 | vgerSave(vger); 211 | vgerTranslate(vger, float2{256,256}); 212 | vgerRotate(vger, M_PI / 20.0); 213 | vgerFillRect(vger, float2{-100,-100}, float2{100,100}, 5.0, cyan); 214 | 215 | vgerRestore(vger); 216 | 217 | [self checkRender:vger name:@"rotate.png"]; 218 | 219 | vgerDelete(vger); 220 | } 221 | 222 | - (void) testRects { 223 | 224 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 225 | assert(vger); 226 | 227 | vgerBegin(vger, 512, 512, 1.0); 228 | 229 | vgerSave(vger); 230 | 231 | auto white = vgerColorPaint(vger, float4(1)); 232 | 233 | for(float i=0;i<10;++i) { 234 | float2 p = {20+i*40,20}; 235 | vgerFillRect(vger, p, p + float2(20), 0.3, white); 236 | } 237 | 238 | vgerRestore(vger); 239 | 240 | [self checkRender:vger name:@"rects.png"]; 241 | 242 | vgerDelete(vger); 243 | } 244 | 245 | - (NSURL*) getImageURL:(NSString*)name { 246 | NSString* path = @"Contents/Resources/vger_vgerTests.bundle/Contents/Resources/images/"; 247 | path = [path stringByAppendingString:name]; 248 | NSBundle* bundle = [NSBundle bundleForClass:self.class]; 249 | return [bundle.bundleURL URLByAppendingPathComponent:path]; 250 | } 251 | 252 | - (id) getTexture:(NSString*)name { 253 | 254 | NSError* error; 255 | id tex = [loader newTextureWithContentsOfURL:[self getImageURL:name] options:nil error:&error]; 256 | assert(error == nil); 257 | return tex; 258 | } 259 | 260 | - (void) testRenderTexture { 261 | 262 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 263 | 264 | vgerBegin(vger, 512, 512, 1.0); 265 | 266 | auto tex = [self getTexture:@"icon-mac-256.png"]; 267 | auto idx = vgerAddMTLTexture(vger, tex); 268 | 269 | auto sz = vgerTextureSize(vger, idx); 270 | XCTAssert(equal(sz, simd_int2(256))); 271 | 272 | vgerFillRect(vger, float2{0,0}, float2{256,256}, 0.3, vgerImagePattern(vger, float2{0,0}, float2{256,256}, 0, false, idx, 1)); 273 | 274 | [self render:vger name:@"texture.png"]; 275 | 276 | vgerDelete(vger); 277 | } 278 | 279 | - (void) testText { 280 | 281 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 282 | 283 | vgerBegin(vger, 512, 512, 1.0); 284 | 285 | vgerSave(vger); 286 | vgerTranslate(vger, float2{100,100}); 287 | vgerText(vger, "This is a test.", float4{0,1,1,1}, VGER_ALIGN_LEFT); 288 | vgerRestore(vger); 289 | 290 | [self checkRender:vger name:@"text.png"]; 291 | 292 | vgerDelete(vger); 293 | 294 | } 295 | 296 | - (void) testScaleText { 297 | 298 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 299 | 300 | vgerBegin(vger, 512, 512, 1.0); 301 | 302 | vgerSave(vger); 303 | vgerTranslate(vger, float2{100,100}); 304 | vgerScale(vger, float2{10,10}); 305 | vgerText(vger, "This is a test.", float4{0,1,1,1}, VGER_ALIGN_LEFT); 306 | vgerRestore(vger); 307 | 308 | [self checkRender:vger name:@"text_scaled.png"]; 309 | 310 | vgerDelete(vger); 311 | 312 | } 313 | 314 | static 315 | vector_float2 rand2() { 316 | return float2{float(rand()) / RAND_MAX, float(rand()) / RAND_MAX}; 317 | } 318 | 319 | static 320 | vector_float2 rand_box() { 321 | return 2*rand2()-1; 322 | } 323 | 324 | static 325 | vector_float4 rand_color() { 326 | return {float(rand()) / RAND_MAX, float(rand()) / RAND_MAX, float(rand()) / RAND_MAX, 1.0}; 327 | } 328 | 329 | - (void) testBezierPerf { 330 | 331 | int N = 10000; 332 | 333 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 334 | 335 | [self measureBlock:^{ 336 | 337 | vgerBegin(vger, 512, 512, 1.0); 338 | 339 | for(int i=0;i& primArray) { 409 | 410 | for(auto& prim : primArray) { 411 | nvgStrokeWidth(vg, prim.width); 412 | auto c = prim.paint.innerColor; 413 | nvgStrokeColor(vg, nvgRGBAf(c.x, c.y, c.z, c.w)); 414 | nvgBeginPath(vg); 415 | auto& cvs = prim.cvs; 416 | nvgMoveTo(vg, cvs[0].x, cvs[0].y); 417 | nvgQuadTo(vg, cvs[1].x, cvs[1].y, cvs[2].x, cvs[2].y); 418 | nvgStroke(vg); 419 | } 420 | 421 | } 422 | 423 | - (void) testNanovgPerf { 424 | 425 | int w = 512, h = 512; 426 | float2 sz = {float(w),float(h)}; 427 | 428 | std::vector primArray; 429 | 430 | int N = 10000; 431 | 432 | for(int i=0;i imageBits(imageLength); 474 | mnvgReadPixels(vg, fb->image, 0, 0, w, h, imageBits.data()); 475 | 476 | auto tmpURL = [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:@"nvg_bezier_perf.png"]; 477 | 478 | NSLog(@"saving to %@", tmpURL); 479 | 480 | writeCGImage(createImage(imageBits.data(), w, h), (__bridge CFURLRef)tmpURL); 481 | 482 | #if TARGET_OS_OSX 483 | system([NSString stringWithFormat:@"open %@", tmpURL.path].UTF8String); 484 | #endif 485 | } 486 | */ 487 | 488 | static void textAt(vgerContext vger, float x, float y, const char* str) { 489 | vgerSave(vger); 490 | vgerTranslate(vger, float2{x, y}); 491 | vgerScale(vger, float2{2,2}); 492 | vgerText(vger, str, float4{0,1,1,1}, VGER_ALIGN_LEFT); 493 | vgerRestore(vger); 494 | } 495 | 496 | - (void) testPrimDemo { 497 | 498 | auto cyan = float4{0,1,1,1}; 499 | auto magenta = float4{1,0,1,1}; 500 | 501 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 502 | 503 | vgerBegin(vger, 800, 800, 1.0); 504 | 505 | auto gradient = vgerLinearGradient(vger, float2{50,0}, float2{100,50}, cyan, magenta, 0); 506 | 507 | vgerSave(vger); 508 | 509 | { 510 | vgerSave(vger); 511 | vgerTranslate(vger, float2{0, 600}); 512 | vgerStrokeBezier(vger, {{50,0}, {100,0}, {100,50}}, 2.0, gradient); 513 | textAt(vger, 150, 0, "Quadratic Bezier stroke"); 514 | vgerRestore(vger); 515 | } 516 | 517 | { 518 | vgerSave(vger); 519 | vgerTranslate(vger, float2{0, 500}); 520 | vgerFillRect(vger, float2{50,0}, float2{100,50}, 10, gradient); 521 | textAt(vger, 150, 0, "Rounded rectangle"); 522 | vgerRestore(vger); 523 | } 524 | 525 | { 526 | vgerSave(vger); 527 | vgerTranslate(vger, float2{0, 400}); 528 | vgerFillCircle(vger, float2{75, 25}, 25, gradient); 529 | textAt(vger, 150, 0, "Circle"); 530 | vgerRestore(vger); 531 | } 532 | 533 | { 534 | vgerSave(vger); 535 | vgerTranslate(vger, float2{0, 300}); 536 | vgerStrokeSegment(vger, float2{50,0}, float2{100,50}, 2.0, gradient); 537 | textAt(vger, 150, 0, "Line segment"); 538 | vgerRestore(vger); 539 | } 540 | 541 | { 542 | vgerSave(vger); 543 | vgerTranslate(vger, float2{0, 200}); 544 | float theta = 0; // orientation 545 | float ap = .5 * M_PI; // aperture size 546 | vgerStrokeArc(vger, float2{75,25}, 25, 2.0, theta, ap, gradient); 547 | textAt(vger, 150, 0, "Arc"); 548 | vgerRestore(vger); 549 | } 550 | 551 | { 552 | vgerSave(vger); 553 | vgerTranslate(vger, float2{50, 100}); 554 | float2 sz {50, 50}; 555 | auto start = sz * rand2(); 556 | vgerMoveTo(vger, start); 557 | for(int i=0;i<3;++i) { 558 | vgerQuadTo(vger, sz * rand2(), sz * rand2()); 559 | } 560 | vgerQuadTo(vger, sz * rand2(), start); 561 | vgerFill(vger, vgerLinearGradient(vger, float2{0,0}, float2{50,50}, cyan, magenta, 0)); 562 | textAt(vger, 100, 0, "Complex Path Fill"); 563 | vgerRestore(vger); 564 | } 565 | 566 | vgerRestore(vger); 567 | 568 | [self checkRender:vger name:@"demo.png"]; 569 | 570 | vgerDelete(vger); 571 | 572 | } 573 | 574 | - (void) testPaint { 575 | 576 | auto p = makeLinearGradient(float2{0,0}, float2{1,0}, float4(0), float4(1), 0); 577 | 578 | XCTAssertTrue(equal(applyPaint(p, float2{0,0}), float4(0))); 579 | XCTAssertTrue(equal(applyPaint(p, float2{.5,0}), float4(.5))); 580 | XCTAssertTrue(equal(applyPaint(p, float2{1,0}), float4(1))); 581 | 582 | p = makeLinearGradient(float2{0,0}, float2{0,1}, float4(0), float4(1), 0); 583 | 584 | XCTAssertTrue(equal(applyPaint(p, float2{0,0}), float4(0))); 585 | XCTAssertTrue(equal(applyPaint(p, float2{0,1}), float4(1))); 586 | 587 | p = makeLinearGradient(float2{1,0}, float2{2,0}, float4(0), float4(1), 0); 588 | XCTAssertTrue(equal(applyPaint(p, float2{0,0}), float4(0))); 589 | XCTAssertTrue(equal(applyPaint(p, float2{1,0}), float4(0))); 590 | XCTAssertTrue(equal(applyPaint(p, float2{1.5,0}), float4(.5))); 591 | XCTAssertTrue(equal(applyPaint(p, float2{2,0}), float4(1))); 592 | XCTAssertTrue(equal(applyPaint(p, float2{3,0}), float4(1))); 593 | 594 | p = makeLinearGradient(float2{1,2}, float2{2,3}, float4(0), float4(1), 0); 595 | XCTAssertTrue(equal(applyPaint(p, float2{1,2}), float4(0))); 596 | XCTAssertTrue(equal(applyPaint(p, float2{2,3}), float4(1))); 597 | 598 | p = makeLinearGradient(float2{400,100}, float2{450, 150}, float4{0,1,1,1}, float4{1,0,1,1}, 0); 599 | XCTAssertTrue(simd_length(applyPaint(p, float2{400,100}) - float4{0,1,1,1}) < 0.001f); 600 | 601 | auto c = applyPaint(p, float2{425,125}); 602 | XCTAssertTrue(simd_length(c - float4{.5,.5,1,1}) < 0.001f); 603 | XCTAssertTrue(equal(applyPaint(p, float2{450,150}), float4{1,0,1,1})); 604 | 605 | p = makeImagePattern(float2{10,10}, float2{10,10}, 0, false, {1}, 1.0); 606 | 607 | auto v = p.xform * float3{10,10,1}; 608 | XCTAssertTrue(equal(v, float3{0,0,1})); 609 | 610 | v = p.xform * float3{20,20,1}; 611 | XCTAssertTrue(equal(v, float3{1,1,1})); 612 | 613 | p = makeRadialGradient(float2(0), 0, 1, float4(0), float4(1), 0); 614 | 615 | XCTAssertTrue(equal(applyPaint(p, float2{0,0}), float4(0))); 616 | XCTAssertTrue(equal(applyPaint(p, float2{1,0}), float4(1))); 617 | XCTAssertTrue(equal(applyPaint(p, float2{0,1}), float4(1))); 618 | XCTAssertTrue(equal(applyPaint(p, float2{-1,0}), float4(1))); 619 | XCTAssertTrue(equal(applyPaint(p, float2{0,-1}), float4(1))); 620 | XCTAssertTrue(equal(applyPaint(p, float2{1,1}), float4(1))); 621 | 622 | p = makeRadialGradient(float2{1,0}, 0, 1, float4(0), float4(1), 0); 623 | XCTAssertTrue(equal(applyPaint(p, float2{1,0}), float4(0))); 624 | XCTAssertTrue(equal(applyPaint(p, float2{1,1}), float4(1))); 625 | 626 | p = makeRadialGradient(float2{425,225}, 10, 40, float4{0,1,1,1}, float4{1,0,1,1}, 0); 627 | 628 | c = applyPaint(p, float2{425,225}); 629 | XCTAssertTrue(equal(c, float4{0,1,1,1})); 630 | 631 | } 632 | 633 | - (void) testTextAlgin { 634 | 635 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 636 | 637 | vgerBegin(vger, 512, 512, 1.0); 638 | 639 | auto str = "This is center middle aligned."; 640 | 641 | auto commandBuffer = [queue commandBuffer]; 642 | 643 | vgerSave(vger); 644 | vgerTranslate(vger, float2{256, 256}); 645 | vgerScale(vger, float2{2,2}); 646 | 647 | float2 cvs[2]; 648 | vgerTextBounds(vger, str, cvs, cvs+1, VGER_ALIGN_CENTER | VGER_ALIGN_MIDDLE); 649 | vgerFillRect(vger, cvs[0], cvs[1], 0, vgerColorPaint(vger, float4{.2,.2,.2,1.0})); 650 | 651 | vgerText(vger, str, float4(1), VGER_ALIGN_CENTER | VGER_ALIGN_MIDDLE); 652 | 653 | auto magenta = vgerColorPaint(vger, float4{1,0,1,1.0}); 654 | vgerFillCircle(vger, float2{0,0}, 1, magenta); 655 | 656 | vgerRestore(vger); 657 | 658 | [self render:vger name:@"test_align.png"]; 659 | 660 | vgerDelete(vger); 661 | 662 | } 663 | 664 | - (void) testPathFill { 665 | 666 | int w = 512, h = 512; 667 | float2 sz = {float(w),float(h)}; 668 | 669 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 670 | 671 | vgerBegin(vger, w, h, 1.0); 672 | 673 | auto paint = vgerLinearGradient(vger, 0, sz, float4{0,1,1,1}, float4{1,0,1,1}, 0); 674 | auto start = sz * rand2(); 675 | vgerMoveTo(vger, start); 676 | for(int i=0;i<10;++i) { 677 | vgerQuadTo(vger, sz * rand2(), sz * rand2()); 678 | } 679 | vgerQuadTo(vger, sz * rand2(), start); 680 | vgerFill(vger, paint); 681 | 682 | [self render:vger name:@"path_fill.png"]; 683 | 684 | vgerDelete(vger); 685 | 686 | } 687 | 688 | - (void) testEmptyPathFill { 689 | 690 | int w = 512, h = 512; 691 | float2 sz = {float(w),float(h)}; 692 | 693 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 694 | 695 | vgerBegin(vger, w, h, 1.0); 696 | 697 | auto paint = vgerLinearGradient(vger, 0, sz, float4{0,1,1,1}, float4{1,0,1,1}, 0); 698 | vgerFill(vger, paint); 699 | 700 | vgerDelete(vger); 701 | } 702 | 703 | float2 circle(float theta) { 704 | return float2{cosf(theta), sinf(theta)}; 705 | } 706 | 707 | void makeCircle(vgerContext vger, float2 center, float radius) { 708 | 709 | constexpr float n = 50; 710 | 711 | float step = 2*M_PI/float(n); 712 | float2 start = center + radius * float2{1,0}; 713 | vgerMoveTo(vger, start); 714 | for(float i=0;iwidth, image->height); 778 | 779 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 780 | 781 | vgerBegin(vger, 512, 512, 1.0); 782 | 783 | vgerSave(vger); 784 | vgerTranslate(vger, float2{0, 512}); 785 | vgerScale(vger, float2{0.5, -0.5}); 786 | 787 | for (NSVGshape *shape = image->shapes; shape; shape = shape->next) { 788 | 789 | auto c = shape->fill.color; 790 | auto fcolor = float4{ 791 | float((c >> 0) & 0xff), 792 | float((c >> 8) & 0xff), 793 | float((c >> 16) & 0xff), 794 | float((c >> 24) & 0xff) 795 | } * 1.0/255.0; 796 | 797 | auto paint = vgerColorPaint(vger, fcolor); 798 | 799 | for (NSVGpath *path = shape->paths; path; path = path->next) { 800 | float2* pts = (float2*) path->pts; 801 | vgerMoveTo(vger, pts[0]); 802 | for(int i=1; inpts-2; i+=3) { 803 | vgerCubicApproxTo(vger, pts[i], pts[i+1], pts[i+2]); 804 | } 805 | } 806 | 807 | vgerFill(vger, paint); 808 | } 809 | vgerRestore(vger); 810 | // Delete 811 | nsvgDelete(image); 812 | 813 | [self render:vger name:@"tiger.png"]; 814 | 815 | vgerDelete(vger); 816 | } 817 | 818 | - (void) testTigerPerf { 819 | 820 | auto tigerURL = [self getImageURL:@"Ghostscript_Tiger.svg"]; 821 | 822 | auto image = nsvgParseFromFile(tigerURL.path.UTF8String, "px", 96); 823 | 824 | printf("size: %f x %f\n", image->width, image->height); 825 | 826 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 827 | 828 | [self measureBlock:^{ 829 | 830 | vgerBegin(vger, 512, 512, 1.0); 831 | 832 | vgerSave(vger); 833 | vgerTranslate(vger, float2{0, 512}); 834 | vgerScale(vger, float2{0.5, -0.5}); 835 | 836 | for (NSVGshape *shape = image->shapes; shape; shape = shape->next) { 837 | 838 | auto c = shape->fill.color; 839 | auto fcolor = float4{ 840 | float((c >> 0) & 0xff), 841 | float((c >> 8) & 0xff), 842 | float((c >> 16) & 0xff), 843 | float((c >> 24) & 0xff) 844 | } * 1.0/255.0; 845 | 846 | auto paint = vgerColorPaint(vger, fcolor); 847 | 848 | for (NSVGpath *path = shape->paths; path; path = path->next) { 849 | float2* pts = (float2*) path->pts; 850 | vgerMoveTo(vger, pts[0]); 851 | for(int i=1; inpts-2; i+=3) { 852 | vgerCubicApproxTo(vger, pts[i], pts[i+1], pts[i+2]); 853 | } 854 | } 855 | 856 | vgerFill(vger, paint); 857 | } 858 | 859 | vgerRestore(vger); 860 | 861 | auto commandBuffer = [queue commandBuffer]; 862 | 863 | vgerEncode(vger, commandBuffer, pass); 864 | 865 | // Sync texture on macOS 866 | #if TARGET_OS_OSX 867 | auto blitEncoder = [commandBuffer blitCommandEncoder]; 868 | [blitEncoder synchronizeResource:texture]; 869 | [blitEncoder endEncoding]; 870 | #endif 871 | 872 | [commandBuffer commit]; 873 | [commandBuffer waitUntilCompleted]; 874 | }]; 875 | 876 | // Delete 877 | nsvgDelete(image); 878 | 879 | vgerDelete(vger); 880 | } 881 | 882 | - (void) testTextLayoutKey { 883 | 884 | TextLayoutKey keyA{"test", 12, VGER_ALIGN_LEFT}; 885 | TextLayoutKey keyB{"test", 12, VGER_ALIGN_RIGHT}; 886 | 887 | XCTAssertNotEqual(keyA, keyB); 888 | 889 | std::hash hash; 890 | XCTAssertNotEqual(hash(keyA), hash(keyB)); 891 | } 892 | 893 | - (void) testLayers { 894 | 895 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 896 | vgerSetLayerCount(vger, 2); 897 | 898 | vgerBegin(vger, 512, 512, 1.0); 899 | 900 | XCTAssertTrue(equal(vgerTransform(vger, float2{0,0}), float2{0, 0})); 901 | 902 | auto cyan = vgerColorPaint(vger, float4{0,1,1,1}); 903 | auto magenta = vgerColorPaint(vger, float4{1,0,1,1}); 904 | 905 | vgerSetLayer(vger, 1); 906 | vgerFillCircle(vger, float2{80,80}, 20, magenta); 907 | 908 | vgerSetLayer(vger, 0); 909 | vgerFillCircle(vger, float2{80,80}, 30, cyan); 910 | 911 | // Magenta circle should be on top! 912 | [self render:vger name:@"layers.png"]; 913 | 914 | vgerDelete(vger); 915 | } 916 | 917 | - (void) testPathRectangle { 918 | 919 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 920 | 921 | auto w = 0.5f; 922 | auto h = 50.0f; 923 | 924 | vgerBegin(vger, 512, 512, 1.0); 925 | 926 | vgerTranslate(vger, float2{256.0f, 256.0f}); 927 | vgerSave(vger); 928 | vgerRotate(vger, M_PI / 6.0); 929 | vgerMoveTo(vger, float2{-w,-h}); 930 | vgerLineTo(vger, float2{w,-h}); 931 | vgerLineTo(vger, float2{w,h}); 932 | vgerLineTo(vger, float2{-w,h}); 933 | vgerLineTo(vger, float2{-w,-h}); 934 | auto cyan = vgerColorPaint(vger, float4{0,1,1,1}); 935 | vgerFill(vger, cyan); 936 | vgerRestore(vger); 937 | 938 | [self render:vger name:@"square.png"]; 939 | 940 | vgerDelete(vger); 941 | } 942 | 943 | - (void) testHighCurvatureBezierStroke { 944 | 945 | auto vger = vgerNew(0, MTLPixelFormatBGRA8Unorm); 946 | 947 | vgerBegin(vger, 512, 512, 1.0); 948 | 949 | auto red = vgerColorPaint(vger, float4{1,0,0,1}); 950 | auto green = vgerColorPaint(vger, float4{0,1,0,1}); 951 | auto blue = vgerColorPaint(vger, float4{0,0,1,1}); 952 | auto cyan = vgerColorPaint(vger, float4{0,1,1,1}); 953 | auto magenta = vgerColorPaint(vger, float4{1,0,1,1}); 954 | auto yellow = vgerColorPaint(vger, float4{1,1,0,1}); 955 | auto white = vgerColorPaint(vger, float4{1,1,1,1}); 956 | 957 | vgerStrokeBezier(vger, {{100, 200}, {120, 100}, {140, 200}}, 10, red); 958 | 959 | vgerStrokeBezier(vger, {{200, 200}, {201, 150}, {202, 200}}, 8, green); 960 | 961 | vgerStrokeBezier(vger, {{300, 200}, {305, 120}, {310, 200}}, 12, blue); 962 | 963 | vgerStrokeBezier(vger, {{100, 350}, {150, 250}, {200, 350}}, 6, cyan); 964 | 965 | vgerStrokeBezier(vger, {{300, 350}, {350, 280}, {320, 380}}, 8, magenta); 966 | 967 | vgerFillCircle(vger, float2{450, 200}, 5, yellow); // Small filled circles for reference 968 | vgerFillCircle(vger, float2{450, 350}, 4, white); 969 | 970 | [self render:vger name:@"high_curvature_bezier_strokes.png"]; 971 | 972 | vgerDelete(vger); 973 | } 974 | 975 | @end 976 | --------------------------------------------------------------------------------