Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.frag text eol=lf
*.vert text eol=lf
*.glsl text eol=lf
*.sh text eol=lf

/Sources/AdaEngine/AdaEngine.docc/*.png -diff -merge
/Sources/AdaEngine/AdaEngine.docc/*.jpg -diff -merge
Expand Down
49 changes: 49 additions & 0 deletions Demos/LinuxSmoke/LinuxSmoke.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import AdaEngine
@_spi(Internal) import AdaPlatform
import Foundation

@main
struct LinuxSmoke: App {
var body: some AppScene {
WindowGroup {
LinuxSmokeView()
}
.window(
with: UIWindow.Configuration(
title: "AdaEngine Linux",
frame: Rect(x: 80, y: 80, width: 800, height: 600),
minimumSize: Size(width: 480, height: 360)
)
)
}
}

private struct LinuxSmokeView: View {
@State private var clicks = 0
@State private var text = "Hello, Linux!"

var body: some View {
VStack(spacing: 24) {
Text("AdaEngine on Linux")
.font(.system(size: 36))
Text("Resize the window, type a message and press the button.")
TextField("Message", text: $text)
.frame(width: 360, height: 44)
Button("Clicks: \(clicks)") {
clicks += 1
smokeLog("LINUX_SMOKE_CLICK \(clicks) text=\(text)")
}
Text(text)
}
.padding(32)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
.task {
smokeLog("LINUX_SMOKE_READY backend=\(RenderEngine.shared.type)")
}
}
}

private func smokeLog(_ message: String) {
FileHandle.standardOutput.write(Data((message + "\n").utf8))
}
179 changes: 179 additions & 0 deletions Documentation/Linux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Linux development

The Linux backend uses X11 (including XWayland and WSLg) and the existing WebGPU
renderer. `LinuxSmoke` exercises window/input support; Studio Standalone and
AdaExamples games use the same backend.

## Ubuntu 24.04 / WSL 2

Use Swift 6.3.2 for compatibility with the current Swan/Dawn dependency. The
engine's package remains in Swift 6.2 language mode. Install the Linux toolchain
using [Swiftly](https://www.swift.org/install/linux/), then:

```sh
sudo apt-get update
sudo apt-get install -y clang cmake ninja-build pkg-config git python3 unzip \
libx11-dev libasound2-dev libpulse0 libvulkan-dev mesa-vulkan-drivers vulkan-tools \
libcurl4-openssl-dev libicu-dev libxml2-dev libsqlite3-dev libncurses-dev \
libedit-dev libblocksruntime-dev libpython3-dev zip gnupg2
```

Start a desktop session and check `DISPLAY`. WSLg supplies an X11 display without
installing a complete Linux desktop. `vulkaninfo --summary` identifies the GPU;
llvmpipe is a software renderer, useful for correctness checks but not GPU
performance measurements.

Use a Linux scratch directory rather than the Windows `.build` directory:

```sh
export ADAENGINE_SKIP_SWIFTLINT=1
export ADAENGINE_LINUX_SCRATCH="$HOME/adaengine-linux-build"
ADAENGINE_HEADLESS=1 swift build --scratch-path "$ADAENGINE_LINUX_SCRATCH" --target AdaPlatform -j 6
```

`ADAENGINE_SKIP_SWIFTLINT=1` bypasses the build-tool lint plugin only. It is useful
while diagnosing platform compilation when unrelated sources have existing lint
errors. Run lint separately on changed sources before submitting changes.

## Swan compatibility

The current Swan checkout provides a Linux Dawn artifact but omits Linux from
the native `WebGPU` target dependency condition. Stage an independent copy:

```sh
mkdir -p "$HOME/adaengine-linux-tools"
git clone https://github.com/adobe/swan.git "$HOME/adaengine-linux-tools/swan-source"
git -C "$HOME/adaengine-linux-tools/swan-source" checkout --detach 1eb45a22c79439b50e9cde774454450e50147149
python3 script/prepare_swan_linux.py "$HOME/adaengine-linux-tools/swan-source" "$HOME/adaengine-linux-tools/swan"
export ADAENGINE_SWAN_PACKAGE_PATH="$HOME/adaengine-linux-tools/swan"
export ADAENGINE_TINT_CACHE="$HOME/adaengine-linux-tools/tint"
export TINT_EXECUTABLE="$(python3 script/ensure_tint.py)"
swift run --scratch-path "$ADAENGINE_LINUX_SCRATCH" -j 6 LinuxSmoke
```

The checkout used for this port is Swan commit
`1eb45a22c79439b50e9cde774454450e50147149`, compatible with Swift 6.3.2 and
SwiftSyntax 602 (the same compatibility requirement as `Tools/Tint/README.md`).
An existing compatible checkout can also be passed to the staging helper.
The helper changes
the platform list in its destination and preserves the original dependency.
It refuses unrecognized manifests and unrelated existing destinations.

An already downloaded, checksum-verified Linux Dawn `.artifactbundle` can be
copied into the staged Swan directory and selected with
`SWAN_LOCAL_DAWN=DawnLinux.artifactbundle` (relative to that package). This avoids
downloading it again through SwiftPM, including when Windows and WSL use different
proxy settings.

## Validation

```sh
ADAENGINE_LINUX_WINDOW_TESTS=1 swift test --scratch-path "$ADAENGINE_LINUX_SCRATCH" \
--filter 'LinuxPlatformTests|LinuxRuntimeTests' -j 6
python3 -m unittest discover -s script -p test_prepare_swan_linux.py
python3 script/ensure_tint.py --check
ADAENGINE_SWIFT_JOBS=4 bash script/validate_webgpu.sh
```

The window test requires a working X11 display. Headless rendering can validate
native window creation, resizing and teardown; it does not prove rendered pixels.
For graphical verification run `LinuxSmoke`, edit its message, press its button,
resize the native window and close it through the window manager. The executable
prints its render backend and button actions.

The initial screen implementation reports the primary display at scale 1.
Per-monitor scaling, native Wayland, custom cursor textures and native alert
dialogs require further work. Alerts currently appear in the engine log.

## Verified on 2026-10-17

Ubuntu 24.04 under WSL 2 / WSLg, Swift 6.3.2 and Mesa llvmpipe Vulkan:

- `AdaPlatform` and `LinuxSmoke` build successfully.
- All 5 focused Linux runtime/platform tests pass, including real X11 resize,
keyboard/text delivery, focus-loss releases and `WM_DELETE_WINDOW` teardown.
- All 70 `AdaUtilsTests` / `AdaInputTests` tests pass.
- All 50 checks selected by the current WebGPU validation script pass, including
fresh Tint conversion, sprite pixels, picking, tile occlusion, render graph
diagnostics, visible-list equivalence and transient attachment lifetime. The 3D golden-frame cases are
excluded by their Linux compile guards and are not covered by this result.
- Both Swan staging helper tests pass; the Windows package manifest still loads.
- The native demo displays its UI, accepts Latin and Cyrillic text, responds to
button clicks, resizes and exits on a native close request.

This is software Vulkan validation. Hardware GPU behavior and a standalone Linux
desktop installation have not yet been verified. The complete engine test suite
has not been run; the module and platform checks above are the tested scope.

## Studio Standalone and games

Install `zenity` for the desktop file/folder chooser. Studio uses the shared AdaUI
workbench, native X11 windows and the Linux run destination. Swift projects and
AdaScript Run in a separate game window are enabled in this Standalone distribution.

The compiler bundled under `Sources/gravity-lang` includes the UTF-8 source-length
and VM-owned string fixes. Use it for Studio and script games:

```sh
export ADAENGINE_GRAVITY_PACKAGE_PATH="$PWD/Sources/gravity-lang"
sudo apt-get install -y zenity
bash script/run_studio_linux.sh
```

Keep the Swan, `SWAN_LOCAL_DAWN` and Tint overrides from the earlier setup.
The Studio launcher selects the bundled compiler by default; the environment
override is useful for running standalone sample builds with the same compiler.

The shared AdaScript lexer recognizes CRLF as one newline. This preserves
declarations after `//` comments in Windows-authored projects and keeps diagnostic
line positions correct for LF, CRLF and CR sources.

The launcher builds Release by default, then packages `AdaEditor`, `gravity-lsp`,
`AdaPackageTool`, resources, Swift runtime libraries, Tint, the AdaScript compiler
and a source `BuildSDK` containing Linux Swan/Dawn. The resulting directory,
`.tar.gz` and checksum are under `~/.cache/adaengine/studio-linux/distributions`.
Run the extracted directory's `./ada-studio`; building Swift games additionally
requires the host Swift toolchain and development packages. Override
`ADA_STUDIO_SCRATCH_PATH`, `ADA_STUDIO_CONFIGURATION`, `ADA_STUDIO_VERSION` or
`ADAENGINE_SWIFT_JOBS` when needed. Use an ext4 scratch directory under WSL.

To run catalog games with the current engine without changing their upstream
checkout:

```sh
git clone https://github.com/AdaEngine/AdaExamples.git "$HOME/AdaExamples"
python3 script/run_example_linux.py "$HOME/AdaExamples/Samples/StarQuest" --product StarQuest
python3 script/run_example_linux.py "$HOME/AdaExamples/Samples/Fold" --product Fold
```

The helper makes a separate editable copy under `~/.cache/adaengine/examples-linux`,
selects this engine by local path and migrates Fold's older reflection API names.
Open AdaScript-only samples such as UnfoldTheWorld through Studio's Open Project
dialog. Under WSLg, `GDK_BACKEND=x11` can select X11 for Zenity as well.

The Linux Studio validation passes 13 focused tests serially across distribution, Linux
destination/SDK discovery, UTF-8/CRLF AdaScript compilation, localization, scene
script loading, hot reload and project input delivery. Another 23 compiler tests
pass in an isolated package built from the production compiler sources, including
newline positions and scriptable schema discovery after comments.
The rebased Studio executable and test product build successfully. SwiftPM's
post-build test invocation stalled under WSL; the compiled Swift Testing runner
completed these 13 checks with `--testing-library swift-testing --no-parallel`.
StarQuest and Fold render native windows; Fold's Jump and Turn controls execute
its AdaScript controller. A 45-second Fold run held shared graphics memory near
105 MiB after excluding transient render attachments from the persistent WebGPU
binding cache. This measurement is a short regression check, not a long-run
performance guarantee.

The packaged debug preview launches from `/tmp` with only `/usr/bin:/bin` in
`PATH`, opens UnfoldTheWorld and runs its scripts in a separate game window: the
drone moves to the closed airlock without UI binding errors. The inline scene
viewport's Play currently stays at the authored positions for this sample; use
the top toolbar's Run button. Inline simulation parity is still incomplete.

Linux scope remains limited: native Wayland, remote AdaPlayer, local ACP agents,
debugger, Cloud sign-in and Web/Android export parity have not been established.
The current MCP dependency logs HTTP startup on Linux but does not open its
listener there. Skeletal Garden's 3D preview reproduces a crash on a llvmpipe worker
inside Mesa's `libvulkan_lvp.so`; this sample is not validated as playable on this
software renderer. A hardware Vulkan desktop still needs separate verification.
6 changes: 6 additions & 0 deletions Editor/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ and assess gaps before proposing new features.
open browser guides; they do not perform unattended installation.
- Keep mocks, fixtures, and QA harnesses in test targets or separate executables.
Do not add test-only objects or QA launch hooks to the main app, including Debug builds.
- Linux Standalone uses X11/XWayland and Vulkan. Follow
[../Documentation/Linux.md](../Documentation/Linux.md) and launch through
`../script/run_studio_linux.sh` to stage resources, runtime libraries and BuildSDK.
Use the bundled Gravity compiler and staged Linux Swan overrides for builds/tests;
native dialogs require Zenity. The initial Linux workbench/local Run port does
not establish remote AdaPlayer, ACP, debugger, Cloud or cross-export parity.
- Use an isolated scratch/module cache when necessary; never delete the shared
`.build`. Preserve unrelated engine, demo, and editor changes.
- Debug macOS builds accept `--editor-project=<absolute-path>` to open the
Expand Down
10 changes: 10 additions & 0 deletions Editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ swift build
swift test
```

### Linux

Studio Standalone can be built for X11/XWayland, including WSLg, with Swift 6.3.2
and Vulkan. See [Linux setup and validation](../Documentation/Linux.md) for the
pinned Swan/Dawn, Tint and bundled compiler setup, then run
`bash script/run_studio_linux.sh` from the engine root. It builds and stages the
editor, resources, Swift runtime and source BuildSDK before launching. File
dialogs require Zenity; Linux is the local Run destination. This initial port
does not establish parity for every desktop service or export target.

### Windows

Use Swift 6.3 or newer for the Swan/WebGPU dependency, Visual Studio C++ Build
Expand Down
2 changes: 1 addition & 1 deletion Editor/Sources/AdaEditor/AdaEditorApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ struct AdaEditorApp: App {
.transformAppWorlds { worlds in
worlds.insertResource(ApplicationFramePacing(maximumFramesPerSecond: 120, minimumFramesPerSecond: 120))
}
#elseif os(Windows)
#elseif os(Windows) || os(Linux)
.transformAppWorlds { worlds in
worlds.insertResource(ApplicationFramePacing(
maximumFramesPerSecond: 60,
Expand Down
2 changes: 2 additions & 0 deletions Editor/Sources/AdaEditor/Assets/Localization/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"The Linux file dialog could not be opened.": "The Linux file dialog could not be opened.",
"Install Zenity to use Linux file dialogs.": "Install Zenity to use Linux file dialogs.",
" · Stopped": " · Stopped",
"#RRGGBB": "#RRGGBB",
"#RRGGBBAA": "#RRGGBBAA",
Expand Down
2 changes: 2 additions & 0 deletions Editor/Sources/AdaEditor/Assets/Localization/ru.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"The Linux file dialog could not be opened.": "Не удалось открыть окно выбора файлов в Linux.",
"Install Zenity to use Linux file dialogs.": "Установи Zenity, чтобы выбирать файлы и папки в Linux.",
" · Stopped": " · Остановлено",
"#RRGGBB": "#RRGGBB",
"#RRGGBBAA": "#RRGGBBAA",
Expand Down
4 changes: 3 additions & 1 deletion Editor/Sources/AdaEditor/EditorDistribution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ public enum EditorDistribution: String, Sendable {
case standalone

public static let current: Self = {
#if os(iOS) || os(tvOS) || os(visionOS) || os(Android)
#if os(Linux)
return .standalone
#elseif os(iOS) || os(tvOS) || os(visionOS) || os(Android)
return .appStore
#else
#if os(macOS)
Expand Down
2 changes: 1 addition & 1 deletion Editor/Sources/AdaEditor/EditorMenuBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ enum EditorMenuBar {
) -> MenuItem {
var key = key
var modifiers = modifiers
#if os(Windows)
#if os(Windows) || os(Linux)
if key == .plus { key = .equals }
if modifiers?.contains(.main) == true {
modifiers?.remove(.main)
Expand Down
1 change: 1 addition & 0 deletions Editor/Sources/AdaEditor/ProjectSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ public struct AdaProjectRun: Codable, Equatable, Sendable {
public enum AdaProjectRunDestination: String, Codable, CaseIterable, Equatable, Sendable {
case macOS = "macos"
case windows
case linux
case iPadOS = "ipados"
case visionOS = "visionos"
case web
Expand Down
2 changes: 1 addition & 1 deletion Editor/Sources/AdaEditor/Tooling/EditorBuildSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct EditorBuildSDK: Sendable {
if let explicit = override ?? environment["ADA_STUDIO_SDK"] {
return try load(at: URL(fileURLWithPath: explicit))
}
#if os(Windows)
#if os(Windows) || os(Linux)
let installed = executable.deletingLastPathComponent().appendingPathComponent("BuildSDK")
if FileManager.default.fileExists(atPath: installed.path) {
return try load(at: installed)
Expand Down
2 changes: 1 addition & 1 deletion Editor/Sources/AdaEditor/Tooling/EditorEngineSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ enum EditorEngineSource {
return nil
#endif
}
#elseif os(Windows)
#elseif os(Windows) || os(Linux)
if let executable,
FileManager.default.fileExists(atPath: executable.deletingLastPathComponent().appendingPathComponent("BuildSDK").path) {
root = executable.deletingLastPathComponent().appendingPathComponent("BuildSDK/AdaEngine")
Expand Down
9 changes: 6 additions & 3 deletions Editor/Sources/AdaEditor/Tooling/SwiftTooling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -430,20 +430,23 @@ enum EditorRunDestination: String, CaseIterable, Equatable, Sendable {
case player = "AdaPlayer"
case macOS = "macOS"
case windows = "Windows"
case linux = "Linux"
case iPadOS = "iPadOS"
case visionOS = "visionOS"
case web = "Web"
case android = "Android"

static var availableCases: [Self] {
#if os(macOS)
allCases.filter { $0 != .windows && (EditorDistribution.current.supportsSwiftProjects || $0 != .android) }
allCases.filter { $0 != .windows && $0 != .linux && (EditorDistribution.current.supportsSwiftProjects || $0 != .android) }
#elseif os(Windows)
[.windows, .web, .android, .player]
#elseif os(Linux)
[.linux]
#elseif !canImport(Network)
allCases.filter { $0 != .android && $0 != .player && $0 != .windows }
allCases.filter { $0 != .android && $0 != .player && $0 != .windows && $0 != .linux }
#else
allCases.filter { $0 != .android && $0 != .windows }
allCases.filter { $0 != .android && $0 != .windows && $0 != .linux }
#endif
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ extension EditorInspectorSidebar {
Text("\u{F720}")
.font(AdaEditorMaterialSymbolFont.font(size: 17))
.foregroundColor(theme.editorColors.blue)
Text(value.isEmpty ? L10n.text(L10n.text("Choose scene prefab…")) : value)
Text(value.isEmpty ? L10n.text("Choose scene prefab…") : value)
.font(.system(size: 10, weight: .bold))
.foregroundColor(value.isEmpty ? theme.editorColors.muted : theme.editorColors.text)
.lineLimit(1)
Expand Down
Loading
Loading