Tech Talk
Desktop Development Tool Comparison
Introduction
As we planned to develop OEDcoder desktop utility app, we looked into different solutions to find what would best meet our requirements including technical, financial, performance, interoperability, UX ... etc. There are many options available on macOS and even more when you consider cross-platform solutions. macOS is our primary target platform but we may distribute our app to other platforms potentially (Windows/Linux/ChromeOS). After reviewing and prototyping multiple tools, we wrote up a comparison of our top three choices:
Quick Comparison
Attribute | Qt | Electron | macOS Native |
---|---|---|---|
Pricing | $4260/Year *($499/Year for Small Businesses) | Free | Free *(not including $99/Year developer account) |
License | Qt Commercial License, GPL 2.0, GPL 3.0, LGPL 3.0 | MIT | Proprietary |
Cross-Platform | Yes | Yes | No |
Native UI Widgets | No | No | Yes |
Minimum Memory Usage | 40.2 MB | 95.3 MB | 21.4 MB |
Minimum # of Processes | 1 | 4 | 1 |
Minimum # of Threads | 3 | 64 | 3 |
Minimum Disk Space | ~200 KB (not including dylibs, 47MB with bundled dylibs or 17MB static) | 240 MB | ~200 KB |
Requirements
To set OEDcoder apart from other existing base64 encoding/decoding tools:
-
Simple to useMust be simple to use for all users regardless their technical background
-
High performance
- Must be able to encode and decode quickly with minimal steps
- Must be able to maintain normal desktop operation during use optimizing resource usage including CPU, memory, and disk space
- Must support any input files regardless of file size or batch size
-
Security
- Must use macOS security best practices including notarization, sandboxing and the hardened runtime
- Must not use any online servers, services or untrusted third party libraries
-
PrivacyAll processing must be done on the local device. No one should have access to the data encoded or decoded within our app except the user.
-
First-class support for macOSUse native macOS UI elements and must integrate well with macOS such as drag and drop
-
Support for distribution through the macOS App StoreUse macOS App Store for payment processing, distribution, and reviews
-
DocumentationMust provide clear documentation in-app, available online, and downloadable
-
Cross-platform (Nice to have)Must support macOS with the option to distribute to other platforms
Tool Comparison
Qt
One of the first tools we tried was Qt. Qt has the following benefits:
-
Cross-PlatformQt provides great tools and support across macOS, Windows and Linux. It also supports mobile platforms although that is out of scope for OEDcoder.
-
Choice of Programming LanguageThe "primary" programming language associated with Qt is C++ with support for other language bindings including their own JavaScript-ish Quick/QML language. Python is probably the next best supported language in terms of bindings and there is a list of options to choose from.
-
Commercial support from The Qt CompanyWe don't necessarily need or expect "enterprise" level support for our tools but it is good to know it is an option.
-
Great C++ IDEQt Creator is a great choice for a C++ IDE and the integrated UI designer works well for quickly putting UIs together.
-
Near-native look and feel and OS integrationNative looking UI widgets, dark mode, drag and drop ... etc.
-
High performanceQt App builds for macOS are approximately 200KB (not including dylibs, 47MB with bundled dylibs or 17MB static) and have the same minimum number of threads but a larger minimum memory usage:vs.
-
Abundant documentationThe Qt Company and their Qt Academy provide good learning resources and there are tons of tutorials, books and YouTube videos of varying quality.
The major consideration for using Qt for commercial software is pricing. While it may be possible to use Qt under the LGPL, compliance with the LGPL for commercial software can be tricky. Given OEDcoder is a closed source project, we are reluctant to use Qt under the LGPL even if we could do so legally. Furthermore, it does not have a native look and feel on most platforms.
Electron
The next cross-platform solution we looked into was Electron. We did a small browser based proof-of-concept and it was easy to bundle it into a desktop app. Some of the benefits of Electron include:
-
Cross-PlatformWith good support for macOS, Windows and Linux
-
Choice of Programming LanguageElectron allows using JavaScript, TypeScript or any other language that can compile to JavaScript.
-
Standard layoutBeing able to use HTML and CSS for layout instead of having to learn another proprietary layout system
-
Open Source with corporate backingElectron is backed by Github, which is owned by Microsoft so they have resources to fix bugs and keep things up to date.
-
Good documentationThe main Electron site has well written documentation and there are plenty of other resources available online.
-
Used by high-profile companiesMany high-profile companies use Electron for their apps including Github (for Github desktop), Microsoft (VS Code, Skype, Teams), Figma, Twitch, Slack and others.
While there are many benefits to using Electron, there are also some obvious drawbacks:
-
High resource usage:
-
Disk space usage is huge.A "hello world" Electron app for macOS weighs in at over 240MB for a single CPU architecture.vs.
-
Memory and CPU/thread usage:vs.
-
Heavy weight for a small toolThere are many Electron based apps that perform well (we use VS Code and Github Desktop almost every day). However, it is a tough comparison for developing a light weight tool such as OEDcoder.
-
-
Performance of JavaScript vs C/C++/SwiftJavaScript performance on the V8 engine is good but it can't compare with compiled languages.
-
Non-native look and feelYou can create great looking UIs with HTML and CSS, but in general they won't look "native". Things like supporting Dark Mode in macOS and Windows are possible but not as easy as a native solution.
-
App architectureElectron apps are divided into back-end and front-end processes and you have to use inter-process communication to communicate between the two. It's not a deal breaker but it's certainly not as simple as a monolithic executable.
Native macOS
After reviewing all the different solutions, we decided to focus on providing the best experience for macOS using native tools. We still had choices to make in terms of which programming language and toolkit to use. We tried Swift, both with SwiftUI and Storyboards but ultimately settled on using Objective-C. The main benefits to using Objective-C with AppKit are:
-
Small build sizeWell under 1MB for the entire app
-
Great performanceThe Objective-C frameworks in AppKit are highly optimized and it's tough to beat C in terms of runtime performance. Memory usage is also one of the lowest of the solutions we tried.
-
Native look and feelDrag and drop, dark mode, UI widgets, and color schemes look familiar and work exactly as macOS users expect.
-
Commercial SupportObjective-C, AppKit and Xcode are all "supported" by Apple although individual developers may not get the necessary attention.
-
Easy visual designerStoryboards and Autolayout are relatively easy to use and you can put together a UI quickly.
-
Mixed-bag documentationApple's developer website does have tons of documentation and there are plenty of books, videos and Stack Overflow answers to look through.
Some drawbacks when using macOS native platform:
-
Not cross-platformIf we decide to distribute OEDcoder on Windows or Linux, we will have to write new versions from scratch.
-
Proprietary tools and programming languagesXcode only works on macOS, while Objective-C and Swift may be used outside of macOS/iOS technically but in niche use cases such as GNUStep. Swift has more support on Linux and Windows than Objective-C but the primary focus is always on Apple platforms understandably.
-
XcodeWe do not expect bug free but critical functions such as source control must be reliable. When using the only built-in source control (i.e. Git), the Xcode UI would regularly get out of synch with the Git repository. Closing and re-opening Xcode temporarily "fixes" the issue, but after noticing discrepancies from time to time and have lost some work subsequently, we switched to using Github Desktop for version control.
-
Multiple "official" solutionsApple provides many "official" solutions for macOS development (Swift vs Objective-C, AppKit, Catalyst, Storyboards, SwiftUI etc.). The "recommended" approach is always the "latest and greatest", yet you may need to fall back to older technologies to support certain features or older hardware.
-
Documentation can be hard to findApple provides a lot of documentation but sometimes answers to obscure bugs or "features" are only found in blog posts and WWDC videos from years ago. We got hit by a poorly documented "feature" of macOS without specific details, the macOS Sandbox file limit (what exactly is the limit?). The way it is implemented means there is no definitive answer.
Final Thoughts
The impressive performance of OEDcoder has proven using macOS native development was the right choice to meet our requirements. We would probably go with Qt if OEDcoder had to be cross-platform or if we had a larger tool budget. Electron is great for developers and companies that focus mostly on web development but it is not our case.
If you are a web developer and are asking "What about Wails or Tauri!?! They solve some of Electron's resource usage issues." We tried them but having to use multiple programming languages (Go or Rust for back-end, JavaScript/TypeScript for front-end) increases complexity and context switching. Also, only Electron gives access to the full file path when dragging files from Finder (i.e. our hard requirement). Electron bundles a version of Chrome that has been modified to provide the full file path when dropping files from Finder while Wails and Tauri both use WKWebView on macOS which does not allow access to the full file path.
1. Sprite Shadows in Godot
This is a three part series discussing and demonstrating methods of creating realistic sprite shadows and their performance for mobile platforms.
Part 1 - How to create shadows for sprites in Godot using Sprite3Ds and a DirectionalLight in a 3D environmentPart 2 - How to simulate shadows by using shadow textures
Part 3 - How to optimize the performance of the Sprite2D based solution by minimizing the number of draw calls.
Part 1 - Sprite3D based solution
Shadows for 2D sprites make them pop off the screen and give the scene a sense of depth. Without shadows, the sprites can look too "flat" and get lost in the background of the scene. Ideally, shadows should appear as if there is a light source shining towards the sprites projecting shadows onto the background. When sprites move or rotate, corresponding shadows should also move or rotate in a realistic way.
Below is an example of the desired outcome:
All projects discussed here can be found on GitHub.
A straight forward way to accomplish shadows for sprites in Godot Engine is to use a 3D scene with Sprite3D objects, a DirectionalLight and a QuadMesh for a background. An example of such a project can be found in the GitHub repo for this series in the directory called "Sprite3dShadows". The project is relatively simple. The only tricky part is that the Sprite3D objects have to have the "Alpha Cut" flag set to "Opaque Pre-Pass" to make the sprites cast shadows on the background.
You can see what the shadows look like below:
There are two problems with this solution:
- The shadows don't look very good. In the example project I kept most of the light and shadow related settings at their default, so there may be ways to make the shadows look better, though probably at the cost of decreased performance.
- 150 draw calls are required for only 25 visible objects in the scene (i.e. 24 fish + the background quad). That might not seem like a problem for desktop games as desktops (especially gaming desktops) are capable of drawing many more than 150 draw calls without any issues. However, when designing games for mobile platforms, the number of draw calls has a significant impact on performance. The mobile game below would not be possible using this technique as it would require hundreds to thousands of draw calls to support so many sprites with shadows.
Between the ugly shadows and poor performance of this solution, it doesn't seem like a viable
option for creating shadows for sprites on mobile.
In the next post, I will introduce a better way to give shadows to sprites by simulating them with textures in a 2D scene.
Part 2 - Shadow Simulation
In my previous post (Part 1), I showed how Sprite3D objects can be used to create shadows for sprites in Godot. There were problems with the Sprite3D based solution. Let's try to solve those problems by simulating shadows using textures in a 2D scene in this post.
Typical 2D Sprite Shadows
The typical way to add shadows to a 2D sprite would be to add the shadows directly to the sprite textures themselves. The problem with this approach is that if the sprites rotate, the illusion of the shadows can be broken as the shadows move with the rotation of the main sprite. An example of what this looks like can be seen below:
The project described above can be found in the "Sprite2dShadowsUnrealistic" folder on GitHub.
More Realistic 2D Sprite Shadows
The shadows can be made more realistic by separating the shadow texture from the main color texture for each object. In the "Sprite2dShadowsRealistic" project, you can see how a Fish object can be created that has both a "Color Sprite" and a "Shadow Sprite". The Fish objects have an attached script that updates the global position and rotation of the "Shadow Sprite" to make the shadow appear as if it is being cast by a fixed light source. An example of what this looks like can be seen below:
These shadows look a lot better than the shadows created with Sprite3D objects. The performance
is also better at 49 draw calls (i.e. 2 for each Fish object and 1 for the ColorRect background).
However, performance could still be a problem on mobile, especially if there are hundreds of
sprites instead of only 24.
In the next post, I will describe how to solve the remaining issue by utilizing a TextureAtlas.
Part 3 - Performance Improvement using a TextureAtlas
In the previous post (Part 2), I showed how realistic looking shadows could be created for sprites by using textures in a 2D scene. While the shadows created look much better than the Sprite3D based solution from (Part 1), the problem of the relatively large number of draw calls remains. 2 draw calls per sprite in the previous post is a significant improvement over 6 draw calls per sprite in (Part 1), but we can still do better.
One key to improving performance for 2D sprites in Godot (or in any game engine that uses OpenGL ES) is to use draw call batching to minimize the number of draw calls for the sprites in the scene. In Godot, the way to enable draw call batching is to import the textures used for the sprites as a TextureAtlas. An explanation of optimization using batching in Godot can be found here. The process for importing textures as a TextureAtlas in Godot is as follows:
- Select the sprites you would like to group into an atlas in the FileSystem dock.
- Go to the "Import" tab of the "Scene" dock and change the "Import As" drop-down from "Texture" to "TextureAtlas"
- Click the folder icon next to "Atlas File" and select the name and location of the atlas file you would like to create
- Click "Reimport"
Godot will need to close and re-open the editor to enable the TextureAtlas. After the atlas is created, whenever you assign a texture to a Sprite the atlas will be used instead at runtime.
The "Sprite2dShadowsRealisticWithAtlas" folder contains a project with the sprites grouped into a TextureAtlas. The use of the TextureAtlas decreased the number of draw calls from 49 (or 2 per Fish + 1 for the background) to just 2 (1 for all of the Fish + 1 for the background). You can see the results below:
Conclusion
In this series, I demonstrated how to create realistic looking shadows for sprites in Godot Engine. The solution with the best performance is to use 2D Sprites with separate shadow textures and a TextureAtlas. The performance aspect may not seem important if you target desktop platforms, however, for mobile game development optimizing graphics performance can have a huge impact. Optimizing draw calls for mobile (OpenGL ES based) games improves overall performance, reduces battery consumption and gives more head room for additional graphics, animations, and effects.
These techniques were used in our mobile game BebeBoop available on the App Store. Turn it up to "Bird Speed" and you can understand how important it is to do draw call optimization, especially when running in "Marathon Mode".
Bonus Part 4 - Draw Call Batching Continued
In the previous post (Part 3), I showed how important draw call batching is to the performance of OpenGL ES based mobile games. In this post I show another example where draw call batching enables graphics in a mobile game that otherwise wouldn't be feasible.
Our game BebeBoop (available in the App Store for iOS) has a "Poster" called "Noodles" that features a real-time hourglass in the background made up of the hourglass itself and 180 individual "grains" (noodle bowl toppings such as peas and carrots). The "grains" are all Godot RigidBody2D based objects with their associated sprites, collision shapes ... etc. Between the hourglass in the background and the other objects in the scene, there are hundreds of sprites (3D objects rendered to 2D) all moving according to physics and interacting with each other. The video below provides a good demonstration:
While testing BebeBoop I regularly watch the performance monitors (found on the "Monitors" tab of the "Debugger" panel) watching for resource leaks and sub-optimal draw-calls. The screenshots below demonstrate the difference in the number of draw calls with and without batching and at different play speeds.
In both cases there were thousands of objects with hundreds of visible sprites. With batching enabled (and TextureAtlases properly configured, as described in Part 3 there were only two to three dozen draw calls. Without batching the same scene requires several hundred draw calls, that is over 1,100% more draw calls for the same graphics. On a desktop PC several hundred draw calls is nothing, however, on a mobile device two hundred plus draw calls will have a much larger impact on frame rate and battery consumption.
2. Godot Add-on - Custom Image Equalizer
Custom Image Equalizer is an add-on for Godot Engine providing a graphical equalizer Control using a custom image as the graphical elements of the equalizer. See below for a demo:
This has been tested on version 3.5.2-stable and uses GDScript. Source code and instructions for use can be found on GitHub.
This add-on was used (in a modified form) in our mobile game BebeBoop in the Posters "DJ Meow Meow" and "Raining Teddies" as well as the Karaoke version of "Raining Teddies" on YouTube.