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:

  • Qt with C++
  • Electron with JavaScript/TypeScript
  • macOS Native (in Swift and/or Objective-C)

Quick Comparison

Comparing attributes of Qt, Electron and macOS native development
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 use
    Must 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
  • Privacy
    All 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 macOS
    Use native macOS UI elements and must integrate well with macOS such as drag and drop
  • Support for distribution through the macOS App Store
    Use macOS App Store for payment processing, distribution, and reviews
  • Documentation
    Must 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
Here is the finished product demo:

Tool Comparison

Qt

One of the first tools we tried was Qt. Qt has the following benefits:

  • Cross-Platform
    Qt 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 Language
    The "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 Company
    We don't necessarily need or expect "enterprise" level support for our tools but it is good to know it is an option.
  • Great C++ IDE
    Qt 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 integration
    Native looking UI widgets, dark mode, drag and drop ... etc.
  • High performance
    Qt 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:
    Qt Memory Usage on macOS
    Qt
    vs.
    Appkit/Objective-C Usage on macOS
    Objective-C
    For a minimal "Hello, World" app, Qt uses twice as much memory than Objective-C
  • Abundant documentation
    The 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-Platform
    With good support for macOS, Windows and Linux
  • Choice of Programming Language
    Electron allows using JavaScript, TypeScript or any other language that can compile to JavaScript.
  • Standard layout
    Being able to use HTML and CSS for layout instead of having to learn another proprietary layout system
  • Open Source with corporate backing
    Electron is backed by Github, which is owned by Microsoft so they have resources to fix bugs and keep things up to date.
  • Good documentation
    The main Electron site has well written documentation and there are plenty of other resources available online.
  • Used by high-profile companies
    Many 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.

      Electron Disk Usage on macOS
      Electron
      vs.
      Objective-C Disk Usage on macOS
      Objective-C
      For a minimal "Hello, World" app, Electron uses 120,000% more disk space than a minimal Objective-C app 😓
    • Memory and CPU/thread usage:
      Electron Memory Usage on macOS
      Electron
      vs.
      Objective-C Memory Usage on macOS
      Objective-C
      For a minimal "Hello, World" app, Electron uses over 400% memory than a minimal Objective-C app 😓
    • Heavy weight for a small tool
      There 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++/Swift
    JavaScript performance on the V8 engine is good but it can't compare with compiled languages.
  • Non-native look and feel
    You 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 architecture
    Electron 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 size
    Well under 1MB for the entire app
  • Great performance
    The 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 feel
    Drag and drop, dark mode, UI widgets, and color schemes look familiar and work exactly as macOS users expect.
  • Commercial Support
    Objective-C, AppKit and Xcode are all "supported" by Apple although individual developers may not get the necessary attention.
  • Easy visual designer
    Storyboards and Autolayout are relatively easy to use and you can put together a UI quickly.
  • Mixed-bag documentation
    Apple'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-platform
    If we decide to distribute OEDcoder on Windows or Linux, we will have to write new versions from scratch.
  • Proprietary tools and programming languages
    Xcode 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.
  • Xcode
    We 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" solutions
    Apple 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 find
    Apple 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 environment
Part 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:

  1. 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.
  2. 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:

  1. Select the sprites you would like to group into an atlas in the FileSystem dock.
  2. Go to the "Import" tab of the "Scene" dock and change the "Import As" drop-down from "Texture" to "TextureAtlas"
  3. Click the folder icon next to "Atlas File" and select the name and location of the atlas file you would like to create
  4. 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.

Performance monitors with batching, normal speed
With draw-call batching, any speed
Performance monitors without batching, normal speed
Without draw-call batching, normal ("cat") speed
Performance monitors without batching, bird speed
Without draw-call batching, fastest ("bird") speed with 1K+ additional objects

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.