I have been working on rewriting the Chunky UI using JavaFX. After a few months of work I am nearly done with the rewrite and it has resulted in removing more than 7 thousand lines of code. That’s close to 15% of the total source code removed!
During the JavaFX rewrite i changed much more than just the UI code. In this post I will summarize the major things that I rewrote or refactored as part of the JavaFX rewrite.
Technical Debt and Feature Creep
It is a common phenomenon that projects with a long development history and changing requirements tend to decay over time. Chunky is no exception and has both technical debt and feature creep. While I was rewriting the UI code I tried to clean up the rest of the code to reduce the technical debt.
I reduced feature creep by removing some rarely-used or unimportant features. This reduced the complexity of the main codebase. My plan is to re-implement some of the removed features as plugins after I have implemented a Plugin API.
I have matured a lot as a programmer since I started working on Chunky. I noticed several things in the code that I would have designed differently nowadays. Here is a short list of things which I have changed my mind about since I started writing Chunky:
- Pointless comments. It is better to write few high quality comments which explain the non-obvious behaviors and dependencies, rather than writing many low quality comments.
- Avoid abstractions. Abstractions are useful if you have many variants implementing similar behavior, or if you are making a public API. However, abstractions make the code harder to read and should be avoided when not necessary. Making abstractions is an attempt to guard against future technical debt, but it seldom works as planned.
- Removing unused code. Sometimes in the past I would keep unused code around because it might be useful in the future. That’s never the case, and the unused code makes it harder to read the rest of the code. Unused code also absorbs pointless maintenance work like refactoring effort, so it’s better to just remove it.
- Not everything should be a class. I would often write small classes that have a few methods but no data. This type of “Doer” class is pointless most of the time. Java 8 makes it very easy to avoid “Doer” classes – use lambdas instead. For common utility functions it can still be useful to keep them in an empty class.
It is usually healthy to analyze the design of your code and try to improve it, but it requires much effort and is usually not immediately rewarding. It is also difficult to clean up old code that you have not touched in a long time, because you have to re-learn how the code works. At least it is easier to clean up your own old code than someone else’s code.
Most of the work during the JavaFX rewrite was spent on rewriting Swing code to JavaFX. This is a tedious and error-prone process. It is similar to copy-paste coding because the code shape has a similar shape after the rewrite, most changes are just API calls that changed name and ordering.
JavaFX does have a nicer API than Swing, but every API change meant extra work in porting the old code to JavaFX. There were no major paradigm shifts, except for the way images are drawn to a canvas in JavaFX versus Swing. I’ll go into more details on that below.
Chunky has dozens of small UI elements: custom widgets, dialog windows, and even a custom color picker. Each UI element has to be connected to code controlling what happens when the widget is modified. Each UI element then has to be checked to see that it works as it should in the new version. I have tried to manually check the behavior of all of the UI elements in Chunky, but there may still be bugs that I missed, either because I forgot to test a particular user interaction, or because later code modifications broke something I already thought was working.
I spent a couple days writing my own color picker for JavaFX because the default JavaFX color picker did not behave exactly as I wanted it to. I am very happy with the new color picker!
Although JavaFX works well most of the time, I did encounter a few bugs. Thankfully none of the bugs has been a complete showstopper. I found and reported a minor cross platform portability bug. Another bug I encountered was well known and had an easy workaround. Hopefully if more projects start using JavaFX it will lead to more bug fixes and a higher quality UI toolkit.
Switching to Java 8
After switching to JavaFX Chunky requires Java 8 to run. This also means that I can finally use Java 7 and Java 8 features in the Chunky code. Java 8 made it possible to simplify several common code patterns:
- Lambdas and method references are used for simple listeners in the UI code.
- Try-with-resources simplifies IO handling.
- The diamond operator helps clean up code using collections and other generic types.
These are not huge changes, but on the whole they improve the readability of the code quite a bit. Apart from just looking cleaner I believe the improved readability will help reduce the cost of maintenance in the future.
Designing for Plugins
I want to add a Plugin system to Chunky. To be able to make useful plugins it is necessary to make an API the plugins can access to change the behavior of Chunky.
During the JavaFX rewrite I made some changes to the rendering design which are aimed at both making the rendering pipeline simpler and to create a new renderer as a plugin:
- Most of the code that depends on a renderer now only uses a public interface rather than directly accessing the default renderer implementation.
- An API has been introduced for the renderer to access the current scene state. The scene state needs to be immutable during rendering, so the renderer is responsible for copying all scene state it needs to use.
It is not entirely clear to me which interfaces are needed for a plugin to be able to inject its own renderer, so I added only the ones that I think are absolutely necessary. I plan to implement a few plugins which will help define the requirements for the plugin API. I don’t want to put too much design into the plugin API before I know exactly what is needed – I don’t want to add more abstractions based on speculation.
The plugin API is not a major goal for the JavaFX update, and the API will probably need to evolve over a couple of releases before it is really useful and stable.
Refactoring the Rendering Pipeline
I refactored the way UI code and rendering code interact with the scene state. The old design allowed the scene state to be modified while it was concurrently copied to the render worker threads. This meant that the render threads would sometimes not read the latest scene state or in the worst case the scene state could appear corrupt for the render threads. There was also a fairly complicated locking regime used to ensure mutual exclusion for important operations like scene saving and loading.
I moved the mutable scene state out of the renderer into a separate class which is only responsible for encapsulating scene state and coordinating writes and reads. The renderer now calls a single method in that class to access the scene state, and that method takes a lock which protects the scene state from concurrent modification from the UI thread, and the scene saving and loading thread. The locking is a bit simpler now, but I think I can simplify it even more in the future.
Not all scene state is copied to the rendering thread. For example, to save memory there is only a single sample buffer. Having a single sample buffer works because it is only the rendering threads which modify it, and they can coordinate themselves so they don’t write to the same regions of the frame. To ensure the sample buffer is not modified while saving the scene I introduced a callback which is called by the renderer when it has completed a frame. The callback can then decide if it should save the current sample buffer. The end-of-frame callback runs on the main rendering thread between frames so there is no risk of concurrent modification while saving the sample buffer to a render dump.
There are still some complex interactions in the rendering pipeline, but it is a bit better organized right now. In the future it might be a good idea to try to separate the scene state from the scene graph and the sample buffer, because the scene graph and sample buffer are generally not synchronized and copied like the rest of the scene state even though they are conceptually part of the scene state.
Painting on a JavaFX Canvas
If you want to draw something on a canvas in Swing you have to implement a callback which is called by Swing when it wants to repaint part of the canvas. Swing only draws the parts of the canvas that are visible: if another window obscures the canvas Swing will not draw the obscured region. When the canvas is revealed again Swing tells your callback to redraw the canvas. If you don’t redraw the canvas you might end up with parts of the canvas being blank after moving the parent window around, or after moving other windows over your canvas.
JavaFX has a much simpler canvas drawing model. You tell JavaFX what should be drawn on the canvas, and then JavaFX ensures that whatever part of the canvas is visible will always show the content you painted on it.
The callback-oriented model of Swing had many disadvantages which disappeared when switching to JavaFX. In the callback-oriented model you could only suggest that a canvas is redrawn and then wait for the Swing thread to call your callback. You could not repaint a canvas directly. The render preview window had to repaint the same rendered frame potentially multiple times, instead of just painting it once when a new frame had been rendered. The 2D map required caching to be able to efficiently draw the same view multiple times. Caching of the 2D map is still used, but now it’s used on a per-chunk basis instead which simplified the 2D map code a lot.