First year of Playnote

On this day in 2025, the evening of my birthday, I decided to make a vision I’ve had for a long time into reality – the next BMS client, not just an incremental update but a reimagining of what BMS can be. I gave myself a timeline of 2 years until version 1.0 release. We’re now halfway there. Let’s look back at what’s done and what’s still left to do.

A cutting-edge foundation

Playnote runs on a bespoke Vulkan engine, in which everything is a vector shape, including text. The graphics are scalable to any size, and hyperoptimized for the lowest possible video latency.

The audio engine is, likewise, built from scratch. Interfacing directly with WASAPI, PipeWire or ASIO, the game requests the lowest possible audio latency by default, and processes input events at their exact timestamps for responsive, sample-accurate playback at any framerate. Due to this design, there is no benefit from running the game at a framerate beyond your screen’s refresh rate.

Input handling runs on a separate thread for a consistently high polling rate. Controllers with either analogue and digital turntables are supported, with denoising of the analogue signal to prevent misfires, and debouncing on buttons.

Charts and songs

Songs are imported into a library in a process that reencodes WAVs to OGG to save space, and packages them into a single zip-based archive per song for blazingly fast random access to any file inside. The process also renders out preview audio, and calculates loudness to normalize volume and prevent loud surprises.

The import process and chart loading are both heavily multithreaded via a custom-built async runtime (including async disk I/O), optimized via thread priorities and core affinities until any background jobs don’t cause a single frame to drop. An average BMS chart loads in under one tenth of a second; by the time the animation of sliding in the playfield is finished, the chart is already loaded.

UI, animation, design

The push for ever more convenient and elegant UI handling has eventually driven the design of a 2D layout engine with dynamic reflow. Everything you see on the screen is the outcome of calculating the final positions of a hierarchy of widgets. All of their properties can be driven by keyframe animations for smooth transitions and other effects. The widgets range from generic layout building blocks (lists, grids), through content containers (text) or interactive components (scrolling lists, knobs), to bespoke game elements (the playfield).

This works together with the renderer’s universal scaling to maintain the same physical size of content on any screen. The widgets are all trivially responsive, and match the display’s DPI.

Cool, but where’s the next milestone, Tearnote?

Yeah, it’s been a while. The next version, 0.2, is about… three quarters done. I promised scoring, but that had to be pushed to a later milestone. Instead, what’s coming next is the theming system. Without spoiling much, I can promise that it will be extremely extensive and a joy to create for, while ensuring that the integrity of the game is maintained.

The system has grown much larger than initially intended, but by now has mostly converged on a very elegant design. However, the work has been delayed somewhat by the fact that…

We got hacked!

TeamCity, the CI orchestrator we used for automated testing on every supported platform, was breached via an auth-bypass vulnerability. The attacker (thankfully?) installed a crypto miner, so the breach was easy to detect via massive resource usage on the server. TeamCity was run by a dedicated, low-privilege system account, so the server as a whole was unaffected, though the TeamCity install itself had to be nuked, together with all files ever touched by that account. I found no evidence of any attempts at privilege escalation, publishing of malicious artifacts, pushing suspicious code to the repositories, or abuse of Google Compute Engine resources.

JetBrains has handled the situation in an irresponsible, dangerous manner that put every user of their software at risk. Despite the severity of the vulnerability, they have publicly announced it on the same day the patches were released, kicking off a race between server owners and black-hat hackers. Exploitation started almost immediately. This is the second time this happened, and the first time I lost the race.

Rest assured no JetBrains software will be used in Playnote’s development or infrastructure going forward.

We have a domain!

playnote.dev is live! Well, it’s a placeholder for now. The website, deployed via the Zola static site generator, is intended to eventually subsume the releases page, the wiki, and the Playnote blogposts. Work on it will start likely after v0.2 release.

We have also migrated Git hosts again, from Codeberg to a self-hosted Forgejo instance at git.playnote.dev, due to a concerning direction of Codeberg’s site policies. Account creation is disabled. If you are interested in making a contribution, hit me up on the game’s Discord and we can have a chat about it.

Due to the hack, CI is moving from TeamCity to Forgejo Actions. This is a highly non-trivial task, due to the fact othat there is no suitable autoscaler for Forgejo Actions at the moment. An autoscaler is a component that spawns VMs for building the game when needed, and destroys them afterwards. It’s essential to minimize the amount of time the VMs exist, due to the fact that running a Windows VM 24/7 would cost over $300 a month. I’m writing an autoscaler myself, in Rust; you should start seeing CI results again on new commits soon.

I’m also taking this opportunity to recreate the builder VMs, this time via scripts rather than installing stuff manually through the remote desktop. This should make the whole infrastructure easier to update and reproduce in the future.

With the removal of TeamCity, Playnote is now developed and hosted entirely on FOSS.

Conclusions

That’s it, I suppose. Happy 1st birthday, Playnote. See you in the next Progress Report.