VexelRay
Framework docs

Getting started

This takes you from nothing to a running VexelRay application, then through what was generated and how to grow it into your own. Allow about half an hour, most of it the first build of the stack.

1. What you need

   
Windows 10 or 11, or Linux Windows is the most complete. Linux builds and runs; macOS is a skeleton
A GPU with a Vulkan driver Any recent NVIDIA, AMD or Intel driver has one
JDK 25 A GraalVM 25 JDK if you also want native executables (step 6)
Maven and Git On PATH. On Windows, Git for Windows also brings the Bash the commands below are written for
A Rust toolchain From rustup.rs. One repository’s image decoders are Rust, built from source
Visual Studio Build Tools Windows only. Rust’s default Windows toolchain links with them; native-image needs link.exe and the icon step rc.exe

2. Install the stack

The stack is not on Maven Central yet. Each piece is its own repository, and they are built and installed to your local Maven repository (~/.m2) once, side by side in one folder. One script does it:

mkdir vexelray && cd vexelray
git clone https://github.com/sibarum/vexelray-framework.git
./vexelray-framework/bootstrap.sh

It checks for the tools above, clones the other ten repositories beside vexelray-framework, builds imagelib-wrapper’s native library for your OS, and runs mvn install in each, in dependency order. Each build’s output goes to vexelray-framework/target/bootstrap/, and the first failure stops it and names its log. --tests runs each repository’s tests as it goes; --check only says what is missing.

When you pull a newer version of one repository, reinstall it and everything after it:

./vexelray-framework/bootstrap.sh --from vexelray

The order, and what each repository is. The repositories says more about each, and lists the optional ones that add pictures and video, shipping, and simulations.

Repository What it is
vexelray-build The Maven plugin that stamps each build with its exact source. Every other build uses it
intrabuf Serialization the message bus carries its envelopes in
atchung The typed message bus, and the logger nearly every repository uses
supirvast The expression language the engine’s shaders are written in
dxgi-bridge Presentation on Windows
kronometer Time and animation
tactroller Input: keyboard, pointer, clipboard
vexelray The engine: Vulkan, 2D drawing, text, windowing
imagelib-wrapper Image decoding, in Rust behind a small Java API. Your application does not link it; vexelray-gui’s icon plugin reads icon art with it
vexelray-gui The widgets, layout and theme
vexelray-framework This repository

imagelib-wrapper’s native library is built from source by its OS module and is not in the repository. The script runs imagelib-wrapper-windows/native/build.sh (or -linux/) before that repository’s install; run it by hand if you install without the script.

3. Generate a project

From the vexelray-framework folder:

java -cp vexelray-framework-template/target/classes dev.vexelray.framework.template.Generate --in .. --name my-app --group dev.example

That writes ../my-app, and its last line is the command that runs it. On Linux the classes are in target-linux/classes instead. --name is the folder and the Maven artifactId; the Java package becomes dev.example.myapp and the main class MyApp unless you say otherwise. Other options include --title, --width, --height, --packageName and --className; --help lists them all with their defaults.

Before writing anything, the generator looks for the stack in your local Maven repository and names any piece it cannot find (not installed: ...), since a missing mvn install is the likeliest reason a new project will not build. It writes the project anyway: you may be about to install it.

4. Run it

cd ../my-app
mvn compile exec:exec

A window opens with a small text editor in it: tabs for open files, a navigator over the working directory, and Markdown shown in colour. Open the README from the navigator (Ctrl+L puts the caret in its path field), change it, save with Ctrl+S, close the window and run it again; it comes back where you left it.

A text editor is the starting point because it is the smallest real application that touches everything a real one does: animation, background work, a question before closing with unsaved changes, settings that persist, and a user interface that tests can build without a window. You will change most of it, and keep its shape.

mvn test

runs the tests that came with it, including one that builds the real window contents with no GPU at all.

5. Find your way around

Read these in this order:

File What it is
MyApp.java The entry point and a few constants (name, title, size, icon). Nothing else
MyAppApp.java The @VexelApp declaration: the name, the window, and the starters (the automation sockets, step 6)
Recipes.java What the application is made of. One @Provides method per part. New parts go here
Model.java, Doc.java All of the application’s state, as one immutable value that changes by being replaced
Ui.java The window’s contents. Holds no state; it redraws from the model
Actions.java Every command, the questions some of them ask, and the close check
Session.java What is remembered between runs
text/ Code with no user interface in it, in public classes, tested in milliseconds. Your own logic belongs in packages like this

What is not there is the point: opening input, the clipboard, remembering where the window was, the clock, the frame loop, the dialogs, the command line and closing everything in order are the framework’s.

After a build, open target/generated-sources/annotations/.../MyAppAppWiring.java (the @VexelApp class’s name, plus Wiring). It is the code the framework wrote from Recipes.java, and it shows exactly which part is built when.

Adding a part

A part is a method in Recipes. Its parameters are what it needs; the framework supplies them:

@Provides
WordCount wordCount(Model model, Gui gui) {
    return new WordCount(model, gui);
}

There is no ordering to declare. Because this takes a Gui, it is built after the Gui exists; one that took only Model would be built before it. If you ask for something nothing provides, or two parts each need the other, it is a compile error with the reason in it, not a crash at startup. overview.md explains how the order is worked out.

Return an interface when other code should be able to substitute the part. A class of your own package may be returned as it is.

Replacing a default

Five of the framework’s own parts are overridden the same way: Appearance (the look; the generated Recipes already provides one), Dialogs (answer the application’s questions yourself, in a test say), InputBackend, ClipboardBackend and LivenessPolicy (what happens when a lane hangs). When you provide one, the framework’s is never created. The rest of the application edge (window memory, pacing, the command line) is not replaceable yet.

Settings

A value from the settings file or the command line is a parameter:

@Provides
Highlighter highlighter(@Setting(value = "theme", def = "light") String theme) {
    return new Highlighter(theme);
}

and --theme=dark on the command line overrides it for one launch.

6. Build a native executable

With a GraalVM 25 JDK as JAVA_HOME, on Windows through the generated native-build.cmd (./native-build.cmd from Git Bash). It is mvn inside a Visual Studio developer environment: native-image needs MSVC’s link.exe and the icon step rc.exe, and under Git Bash coreutils’ own link.exe would be found first. Plain mvn works only from a Visual Studio developer prompt.

./native-build.cmd -Pnative-release package -DskipTests

writes target/my-app.exe, the build you ship: no console window.

./native-build.cmd -Pnative package -DskipTests

writes target/my-app-debug.exe, with a console, for testing: it prints the automation: localhost:<port> line.

On Linux there is one build, with plain mvn:

mvn -Pnative-linux package -DskipTests

writes target-linux/my-app.

Every build carries the automation sockets, remote control and diagnostics, and starts with them off. A person turns them on from the plug in the title bar, which restarts the application with them open; its lamps show each socket open or not and anything connected now or earlier. A launch opens them when it asks, with --automation=<port>, or -Dautomation=<port>, or, when the launch names no automation setting at all, the environment variable VEXELRAY_AUTOMATION=<port> (0 for a free port), which an agent’s shell sets so every application it starts can be driven. That includes mvn compile exec:exec, whose automation property is empty unless you set it. Each run’s port is in its discovery file, ~/.vexelray/automation/<app>-<pid>.auto, and ottermate with no --port reads it. --automation.available=none (or control, or diagnostics; VEXELRAY_AUTOMATION_AVAILABLE in the environment) takes them out of a run altogether, and with neither available the plug is not shown.

The sockets come from one annotation: src/main/java/.../MyAppApp.java declares the app with the automation starter.

The application’s icon is src/main/rc/my-app.ico. It starts as the framework’s “new app” mark; replace that one file and the window, the taskbar button and the .exe all change together. The generated project’s README says how to render one from an SVG.

7. Drive it and take screenshots

ottermate drives a running application the way a person does: keys, clicks, reading what is on screen, and screenshots taken from the application’s own GPU device. It is how a VexelRay application is tested end to end. It lives at vexelray-gui/vexelray-gui-automation-cli/ottermate (ottermate.cmd from cmd or PowerShell), built by vexelray-gui’s install in step 2, and the examples assume that folder is on your PATH. If it says it is not built, run mvn -pl vexelray-gui-automation-cli package in vexelray-gui.

ottermate shot out.png --launch mvn.cmd compile exec:exec -Dautomation=0
ottermate --size 1600x900 --zoom 1.5 shot big.png --launch mvn.cmd compile exec:exec -Dautomation=0

Its guide is ottermate.md in vexelray-gui. The names a script may rely on are the ones in your project’s Landmarks.java. The window remembers its last size, so a changed default size never shows on a machine that has run the app before: give --size whenever you compare screenshots.

8. Logging

The framework sets logging up before your code runs. Running from your checkout, the console shows info and target/logs/my-app.log has debug; a native build logs warnings to the console and info to ~/.my-app/logs/. Log through sibarum.probe.Log, not System.out:

private static final Log LOG = Log.of("my-app.files");

LOG.warn("could not save {}", path, exception);

--log=debug changes the level for one launch.

Where next