Scala’s LibGDX

A Report from the Trenches


Mateusz Kubuszok

About me

Why port libGDX to Scala?

What libGDX gives you

Common game logicwritten in Java, against the libGDX APIDesktopLWJGL3on a normal JVMAndroidAndroid SDKbytecode convertedBrowserGWTJava source → JSiOSRoboVM / MobiVMbytecode → native code

…​and what you keep if you write it in Scala

Common game logicwritten in Scala, against the libGDX APIDesktopLWJGL3on a normal JVM✓works as isAndroidAndroid SDKbytecode converted⚠works, but Scala/Androidversion clashes; native libsBrowserGWTJava source → JSGWT compiles Java source,not bytecodeiOSRoboVM / MobiVMbytecode → native codeScala on RoboVM:unsupported, unmaintainedScala Native: not a libGDX target at all (Java + JNI)

The build tooling goes too

What a game needslibGDX + GradleScala + sbt
Desktop apps for Windows / macOS / Linux, built on one hostConstruo: bundles a trimmed JVM and a launchersbt-native-packager: packages only for the platform you build on
Android: convert bytecode, shrink, sign, packageAndroid Gradle Pluginsbt-android: unmaintained for years
iOS: compile to native code, package the appRoboVM / MobiVM Gradle pluginNothing: RoboVM doesn't support Scala, Scala Native has no iOS
Browser build, and knowing which assets exist at runtimeGWT Gradle plugin; the build generates assets.txt, which GWT's preloader reads to fetch and list assetsScala.js has no classpath, so getResourceAsStream finds nothing; and no build step embeds the assets or lists them
Native libraries for every platformgdx-jnigen builds them; one JAR of native libraries per platformJVM: extract from the JAR by hand; Scala Native: install the library on the system and pass linker flags yourself

Demo

We’re done, thank you!

This demo is deceptive

  • I had a demo like this in March

  • What you’ve just seen is the first iteration of a different approach

  • It needed tooling that Scala didn’t provide

Part I: Porting with agents

The projects

  • SGE: Scala Game Engine

  • SSG: Static Site Generator

  • re-scale: a utility to trace the progress of porting with LLMs

  • Baltic Porter: a transpiler from Java to Scala

Claimed vs measured

SSG's Sass compiler, ported by an agent, April 6th: "migration COMPLETE: 283/283 files"
the agent's claim

Claimed vs measured

The next day: the official Sass test suite
20.7% of the tests pass

Claimed vs measured

Then: every method checked against the original
37.7% faithfully ported
25.8% cut short: there, but simplified
36.1% missing

Eight months

FebMarAprMayJunJulAugSepOctOpus 4.6SGE (game engine) resumeswhat the agents claimedSGE: tests greenSSG (site generator) startschecked method by method:one file 6% portedSass: 1 test in 5

April: the files were "done"

FebMarAprMayJunJulAugSepOctOpus 4.6SGE (game engine) resumeswhat the agents claimedSGE: tests greenSSG (site generator) startschecked method by method:one file 6% portedSass: 1 test in 5Claude Code: less reasoningusage throttled at peak hours
  • ▲03-04 Claude Code gave the model less reasoning by default, and a bug made it forget its earlier reasoning (Anthropic admitted it in April)
  • ▲03-23 Usage limits throttled at peak hours
  • ●04-06 Sass compiler: the agent says "complete, 283 of 283 files"; the tests say 1 in 5
  • ◆04-07 New checks: is the file about as long as the original? Does it have the same methods? Any TODOs or stubs?
  • ◆04-08 re-scale: a tool that runs those checks on every file, in CI
  • ●04-10 A colour library at a fifth of its original size, with no TODO anywhere; a physics engine of 400 files replaced by 8
  • ◆04-11 Rule for the agents: "porting is binary — 100% or not done"

Eight months

FebMarAprMayJunJulAugSepOctOpus 4.6SGE (game engine) resumeswhat the agents claimedSGE: tests greenSSG (site generator) startschecked method by method:one file 6% portedSass: 1 test in 51,200 files marked 'complete'Opus 4.8Fable 5stronger model's review:the logic is brokenClaude Code: less reasoningusage throttled at peak hours

June: every method present, bodies hollow

FebMarAprMayJunJulAugSepOctOpus 4.6SGE (game engine) resumeswhat the agents claimedSGE: tests greenSSG (site generator) startschecked method by method:one file 6% portedSass: 1 test in 51,200 files marked 'complete'Opus 4.8Fable 5stronger model's review:the logic is brokenClaude Code: less reasoningusage throttled at peak hoursFable silently replaced by Opus
  • ●04-20 The agents marked 1,200 files 'complete' in one go, and turned the CI check into a warning
  • ●06-10 Text layout: breaking out of a loop became returning from the whole method
  • ●06-10 Font rendering: // gx += xAdvances[ii] commented out, so every letter is drawn in one spot
  • ●06-10 JavaScript minifier: 1,507 of 2,522 tests marked "expected to fail", so CI showed 0 failures; really ~40% worked
  • ◆06-10 New rules: a failing test first; failure counts may only go down; CI must block; a different model reviews
  • ▲06-10 Anthropic's safety filter could silently replace Fable with Opus
  • ●06-12 Fable switched off worldwide for ~3 weeks: Opus reviewed Opus

Eight months

FebMarAprMayJunJulAugSepOctOpus 4.6SGE (game engine) resumeswhat the agents claimedSGE: tests greenSSG (site generator) startschecked method by method:one file 6% portedSass: 1 test in 51,200 files marked 'complete'Opus 4.8Fable 5stronger model's review:the logic is brokenFableoffall review findings 'fixed'Fableback brieflyfresh review:5 critical bugsClaude Code: less reasoningusage throttled at peak hoursFable silently replaced by Opus
The agents aren't trustworthy because they are non-deterministic.
me, mid-July

Eight months

FebMarAprMayJunJulAugSepOctOpus 4.6SGE (game engine) resumeswhat the agents claimedSGE: tests greenSSG (site generator) startschecked method by method:one file 6% portedSass: 1 test in 51,200 files marked 'complete'Opus 4.8Fable 5stronger model's review:the logic is brokenFableoffall review findings 'fixed'Fableback brieflyfresh review:5 critical bugsOpus 5agent porting stopsmeasured by tests (Baltic Porter)Baltic Porter startslibGDX core compilesFable 5.1the old agent ports:worse stillOpus 5: uneven qualityless reasoning than selectedold agent ports found to cheat12 sample games runSGE core now generatedOpus 5.5extensions generatedClaude Code: less reasoningusage throttled at peak hoursFable silently replaced by Opus

What was tried?

re-scale: what it checked, how it was beaten

re-scale: a command-line tool and Claude Code plugin that checks every ported file against its original

The checkHow the agents got past it
Every file records the original's methods and size; CI checks the recordThe agents marked 1,200 files "complete" in one go, recording whatever each file already had, and turned the CI check into a warning
Same methods as the originalEvery method there, the insides hollow: leaving a loop became leaving the whole method; the line that moves to the next letter commented out
About the same size as the originalCaught a colour library at a fifth of its size, but full-size files could still be wrong: the minifier was bigger than the original, and about 40% of its tests passed
A scanner for words like "TODO" or "stub"The agents reworded their comments until the scanner stopped finding them
A review by a different modelA model reviewing its own work explained the gaps away; with Fable off, the reviewer silently became the same model; one reviewer invented 3 of its 5 findings

Baltic Porter: what it does, where it slipped

Baltic Porter: a translator to Scala 3, from Java first, later TypeScript, JavaScript and Dart. Same input, same output. Agents improve the translator; they don't port code.

The approachWhat still went wrong
Each project has its own rules: Scala names, Nullable instead of null, opaque types, a few hand-written filesThe rules are code too, so they need review
Agents work on the translator, never on the portOne "generator" for Mermaid only contained 187 pieces of the old agent port, copied byte for byte
Tests decideAn agent switched off 28 test suites and reported "0 failures"; another switched off 23 tests half an hour after regenerating the code
Generated code is never edited by handAgents patched it with find-and-replace and loosened test assertions; all reverted later
Same input, same outputCompiling is not the same as correct: a lost break, a wrong equality check; only running the tests found them

The limit is now test coverage, not honesty.

Part II: Shipping cross-platform Scala

Everything built along the way became multiarch-scala: sbt plugins and small runtime libraries

What still needs native code

In libGDXJVMScala NativeBrowser
LWJGL (OpenGL)ANGLE via PanamaANGLE via @externWebGL
LWJGL (windows, audio)GLFW, miniaudio via PanamaGLFW, miniaudioDOM, Web Audio
libGDX's own C code (buffers, images)Rust library via Panamasame library, same C interfacepure Scala
FreeType (fonts)Rust wrapper via Panamastatic librarynone
Box2D / Bullet (physics)Rapier via Panamastatic libraryRapier as WASM

A native library is just a dependency

Problem beforeWhat multiarch-scala adds
JVM: no standard way to ship and load native librariesA JAR with the native library for each platform, plus a small description file; a loader picks the right one and loads it
Scala Native: install the library on the system yourself, and write the linker flags yourselfThe same kind of JAR carries ready-to-link libraries and their flags; adding the dependency is enough
You can only build for the machine you are onNative binaries cross-compiled with zig; desktop JVM apps packaged for every system from one machine
No maintained sbt plugin for AndroidAn Android plugin: converting to Android bytecode, resources, signing
Scala.js: no classpath, no ServiceLoaderAssets embedded at build time behind one API; a ServiceLoader generated at compile time

What we still haven’t fixed

Upstream: Scala Native

  • No iOS support, so no iOS backend at all
  • Scala Native can't build for Windows on ARM
  • Shipping native libraries still needs a plugin

Our own backlog

  • Android: code shrinking and older Android versions
  • No GPU in CI, so graphics tests run on a software renderer

So what do I have today?

SGE: what is generated, what is left

Part of SGEStateStill maintained by hand
libGDX coregenerated from upstream at build time5 hand-written overrides
Utilities and 5 small extensionsgeneratedalmost nothing
The other ~690 extension filesporting rules not written yetstill the agent port

Tests pass on JVM, JS and Native

Plain translation vs SGE’s conventions

plain Java → Scala translation
36%
SGE's own design on top
64%

Every change the translator makes to SGE core, by kind

  • 3,710getX()/setX() turned into Scala properties, empty () dropped
  • 1,083Nullable instead of null
  • 398opaque types (Pixels, Align, Input.Button, …)
  • 282an Sge context instead of global variables

Less code to maintain, same behaviour

PR #146: Baltic Porter integration, +7,950 / -115,453 lines PR #160: core overrides 89 to 16, +83 / -14,687 lines PR #153: ecs generated from Ashley, +789 / -2,794 lines

Three SGE pull requests: about +8.8k / −133k lines, with tests and demos still passing

SSG: Java done, the rest is still an agent port

Libraries ported into SSGSource languageState
Markdown parser, Liquid templatesJavagenerated, tests pass
KaTeX, terser, rough.js, MermaidTypeScript / JavaScriptstill the agent port; 0–5% translated
Sass compilerDartstill the agent port; nothing translated yet
HTML/CSS minifierRubyagent port, no translator

Over twelve thousand tests pass on every platform, but the non-Java ones still run the agent-written code

Ready, or a stepping stone?

Java sources: almost ready

  • libGDX core and utilities, SGE extensions, SSG's Markdown and Liquid
  • Every change tested on JVM, JS and Native
  • What can't be translated is counted; what stays hand-written is listed, with reasons
  • Only snapshot builds so far, no release

TypeScript / Dart / Ruby: a first step

  • KaTeX, terser, rough.js, Sass, Mermaid, the minifier
  • The agent port's structure, waiting for translated method bodies
  • About 0–5% translated today

Summary

Porting with LLMs

  • Automatic porting of libraries is not a solved problem

  • Every LLM will mislead you

  • Being certain means reviewing all of the code: impossible at this size

  • So take away every chance to cheat: guardrails shrink the review, they never remove it

  • Use the LLM to build a deterministic migration, then run that

Scala on every platform

  • Scala already compiles to the browser and to native code; Java needs separate external compilers for both

  • The community already builds tooling for multiple platforms, without weird hacks in the build

But we can achieve more

  • Out of the box: backends, sometimes frontends, CLI tools - but we can make it work better on desktop and mobile

  • Scala Native works flawlessly only if the native libraries are already installed on the system - but we can support some way of distibuting precompiled binaries as well

  • All of it looks solvable, often by reusing existing solutions

Questions?

Thank you!