makepad/platform/build.rs
Kevin Boos bc8c37357a Choose the Linux GPU backend at runtime, and make Vulkan the default (#1237)
* Choose the Linux GPU backend at runtime and pace Wayland frames adaptively

A Vulkan-capable desktop Linux build (the `vulkan` feature, or
`MAKEPAD=vulkan`) now carries OpenGL ES as well and picks between them
when its event loop starts: Vulkan on Wayland when a hardware device
answers, OpenGL ES when none does (no driver, only a software rasterizer,
or an X11 session). `MAKEPAD_GPU=auto|gl|vulkan` overrides the choice, and
`MAKEPAD=gl` still produces an OpenGL-only binary. The feature stays
opt-in: the hosted (`--stdin-loop`) and direct renderers of such a build
are Vulkan-only, and Vulkan has no video texture import yet.

Wayland frame pacing
- Pace presents by what the backend and the session can actually do,
  rather than by a fixed number (new `wayland/frame_pacer.rs`). Vulkan
  runs two presents in flight only when the compositor offers `fifo-v1`
  and the driver uses it; otherwise a second present would block inside
  `vkQueuePresentKHR` on a callback an occluded window never receives.
  OpenGL starts the next frame early only when its measured cost says the
  swap would land after the outstanding callback is due, so cheap frames
  are not committed twice per refresh. One present in flight, which is
  what this did before, left a heavy scene at half the display rate.
- Bound the pacing gate at 250 ms so an occluded window cannot freeze the
  app's clocks, and let pending screenshot requests through it.
- Treat WouldBlock on the display flush as transient.

Vulkan
- Bound the frame fence wait and the swapchain acquire on Linux instead of
  waiting forever.
- Keep the per-frame packet arena mapped, recycle completed frame
  resources on the window path, and ask for one more swapchain image on
  Linux, where the pacing can keep two presents queued.
- Skip CPU devices unless `MAKEPAD_GPU=vulkan` asks for Vulkan explicitly.

OpenGL
- Stop repainting forever at rest: poll the texture lifetime fence once
  per frame, and check for time-driven shaders only after the
  zero-instance skip, as Vulkan does. The explicit
  `Cx::frame_completion_serial` poll still always arms a fence.
- Upload draw-call uniforms only when they changed; they were uploaded
  twice per draw call per frame. A zbias shift now marks them dirty, so a
  call skipped that frame still uploads when it next draws.
- Compute the retained-instance upload plan once per buffer per frame; it
  was computed three times.
- Target remote screenshot requests at the presenting window. Every
  `--remote` grab timed out on OpenGL before this.
- Emit the `gpu.present` trace with render and swap timings.

Retained instances
- `upload_plan` settles segments that kept their slot and offset by `Arc`
  identity, scans for the first few that moved, and only then builds a
  pointer-keyed map. On a large map this took a plan from 0.3-1.5 ms to
  about 0.07 ms. Results are identical to the previous planner.
- Add `collect_backlog` so a renderer can drain retirements once a frame.

Runtime backend consistency
- `CxOs::vulkan_active()` replaces the compile-time branches that decided
  between the two renderers, so a build that fell back to OpenGL releases
  its uniform buffers, shares host swapchains and retires textures the way
  an OpenGL build does.

Wayland teardown
- Drop windows before the `Connection`, and destroy a window's EGL surface
  and `wl_egl_window` before its `wl_surface`. Every OpenGL exit on
  Wayland segfaulted inside NVIDIA's egl-wayland.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Make Vulkan the default on desktop Linux, with mipmaps and hosted fallback

Every desktop Linux binary now carries both renderers and picks at startup,
instead of only the apps that asked for Vulkan by name. Three things had to
be true first.

Gate the feature where the fallback exists. build.rs derived `use_vulkan`
from `target_os == "linux"` alone, which also matches OpenHarmony and every
other Linux triple, none of which carry `naga`, and it ignored
`MAKEPAD=linux_direct`, whose DRM/KMS renderer has no OpenGL fallback of its
own. The feature now only reaches x86_64/aarch64 gnu windowed builds;
`MAKEPAD=linux_direct+vulkan` remains the way to ask for direct Vulkan.

Give Vulkan a mip chain. `image_cache_use_mipmaps` was off for Vulkan
because the uploader only ever filled level 0, so every minified image
aliased. Images now allocate their full chain and fill levels below the
first with `vkCmdBlitImage`, the way `glGenerateMipmap` does, skipping
formats the device cannot linearly blit. On Robrix's sign-in icons this
takes Vulkan from 2153 pixels differing from the OpenGL render by more than
8, to 400.

Choose the hosted renderer at runtime too. `--stdin-loop` mode was
Vulkan-only in a Vulkan-capable build and panicked when no device answered,
while its host, on an X11 session, had already fallen back to OpenGL: with
the feature on by default that combination would have killed every child the
wm launches. The hosted path now selects the way the windowed one does, its
import follows the renderer the process actually started, and a hosted child
rejects software devices for the same reason a window does.

Video and `Texture::read_back` are still OpenGL-only; the video error now
names `MAKEPAD_GPU=gl`, and the feature comment says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* workspace: drop the renderer-routing argument and its build-time wording

One desktop Linux binary now carries both GPU backends and picks at startup,
so an app honouring a saved renderer choice passes it to the platform as
MAKEPAD_GPU and restarts itself. Nothing produces `--renderer-routed` any
more; an argument this parser does not know was already ignored, so dropping
its arm changes nothing for anyone still passing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep the shared Android and direct-display paths as they were

An audit of what this branch reaches on platforms that share these files
found five places where it changed behaviour it was never meant to touch.
All of them come from code this branch made runtime-selected or relaxed.

Mipmaps are desktop Linux only, matching `image_cache_use_mipmaps`, which
is what asks for the format. An Android or Quest Vulkan build shared the
new chain code and would have allocated levels and recorded blits that
nothing there requests and nothing measured.

The mip chain also needs more of the format than it asked for. It checked
only that the format samples linearly, while `record_mip_chain` blits
between levels, so it now requires BLIT_SRC and BLIT_DST too and keeps a
single level otherwise.

Shader compilation stays a compile-time answer off desktop Linux. Whether
a draw shader is compiled to SPIR-V became a runtime `vulkan_active()`
test, which on Quest would follow an Android Vulkan init failure instead
of the build. Only desktop Linux has that fallback.

The hosted loop compiles GLSL only when OpenGL is the renderer. Losing its
cfg left it calling `gl()` in a Vulkan hosted child, which has no EGL
context, so it panicked. Its Wayland sibling already guards this way.

The direct display build keeps its software-buffer upload. `texture_for_draw`
gained a `not(linux_direct)` that was never needed: `MAKEPAD=linux_direct`
without Vulkan has its own `upload_presentable_image_software_buffer` in
os/linux/presentable.rs, and the outer gate already excludes the direct
Vulkan build.

Also: `gpu_preference` is now gated exactly where its caller is compiled,
since `vulkan_linux.rs` builds for every `target_os = "linux"` under
`use_vulkan`, and the hosted loop's imports follow the block that uses
them, which the direct Vulkan build does not compile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 12:14:30 +02:00

224 lines
10 KiB
Rust

use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
/// Best-effort default for the macOS bundle name when MAKEPAD_BUNDLE_NAME isn't
/// set. Cargo doesn't expose the consuming binary's package name to a
/// dependency's build script (`CARGO_PKG_NAME` here is "makepad-platform"), so
/// we walk up from `OUT_DIR` (which is always
/// `<root>/target/<profile>/build/<crate>-<hash>/out`) to the directory that
/// contains `target/` and use that directory's name. For a typical project
/// that's the package or workspace root, which is almost always a meaningful
/// label. Capitalize the first letter so the menu bar shows "Sample app" rather
/// than "sample app". Returns `None` if the path doesn't have the expected shape
/// or the directory name isn't valid UTF-8.
fn detect_app_name(out_dir: &Path) -> Option<String> {
let workspace_root = out_dir.ancestors().nth(5)?;
let dir_name = workspace_root.file_name()?.to_str()?;
if dir_name.is_empty() {
return None;
}
let mut chars = dir_name.chars();
let first = chars.next()?;
Some(first.to_uppercase().collect::<String>() + chars.as_str())
}
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let path = Path::new(&out_dir)
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap();
let cwd = std::env::current_dir().unwrap();
let mut file = File::create(path.join("makepad-platform.path")).unwrap();
file.write_all(format!("{}", cwd.display()).as_bytes())
.unwrap();
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let target = env::var("TARGET").unwrap();
if target_os == "macos" {
// The downstream app can override the bundle name shown in the macOS
// application menu by setting MAKEPAD_BUNDLE_NAME — typically via its
// `.cargo/config.toml` `[env]` section with `force = true`. macOS uses
// CFBundleName from this Info.plist as the first menu bar item title
// for unbundled `cargo run` launches, and it overrides whatever NSMenu
// title we pass to setMainMenu:. When the env var isn't set, we fall
// back to the workspace/package directory name (capitalized), which
// is almost always more meaningful than a hardcoded placeholder.
let bundle_name = env::var("MAKEPAD_BUNDLE_NAME")
.ok()
.or_else(|| detect_app_name(Path::new(&out_dir)))
.unwrap_or_else(|| "Makepad App".to_string());
let bundle_id = env::var("MAKEPAD_BUNDLE_IDENTIFIER")
.unwrap_or_else(|_| format!("dev.makepad.{}", bundle_name.to_lowercase().replace(' ', "-")));
let command_line_plist = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>{bundle_id}</string>
<key>CFBundleName</key>
<string>{bundle_name}</string>
<key>CFBundleDisplayName</key>
<string>{bundle_name}</string>
<key>GCSupportsControllerUserInteraction</key>
<true/>
<key>GCSupportedGameControllers</key>
<array>
<dict>
<key>ProfileName</key>
<string>ExtendedGamepad</string>
</dict>
</array>
<key>NSLocationUsageDescription</key>
<string>Used to show your position on the map.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Used to show your position on the map.</string>
</dict>
</plist>
"#
);
std::fs::write(path.join("Info.plist"), command_line_plist).unwrap();
}
// Per slot: env-var override → auto-discovery in `<workspace_root>/resources/`.
// Workspace root is the dir containing `target/` (5 ancestors up from
// OUT_DIR), same heuristic as `detect_app_name`.
let icons: &[(&str, &str, &str)] = &[
("MAKEPAD_APP_ICON_32", "icon_32.png", "CUSTOM_ICON_PNG_32"),
("MAKEPAD_APP_ICON_64", "icon_64.png", "CUSTOM_ICON_PNG_64"),
("MAKEPAD_APP_ICON_128", "icon_128.png", "CUSTOM_ICON_PNG_128"),
("MAKEPAD_APP_ICON_256", "icon_256.png", "CUSTOM_ICON_PNG_256"),
("MAKEPAD_APP_ICON_512", "icon_512.png", "CUSTOM_ICON_PNG_512"),
("MAKEPAD_APP_ICON_1024", "icon_1024.png", "CUSTOM_ICON_PNG_1024"),
("MAKEPAD_APP_ICON_ICO", "icon.ico", "CUSTOM_ICON_ICO"),
];
let resources_dir = Path::new(&out_dir).ancestors().nth(5).map(|r| r.join("resources"));
let mut icon_gen = String::new();
for &(var, filename, const_name) in icons {
println!("cargo:rerun-if-env-changed={var}");
let path = env::var(var).ok().or_else(|| {
let p = resources_dir.as_ref()?.join(filename);
p.is_file().then(|| p.to_string_lossy().into_owned())
});
let value = match &path {
Some(p) => {
println!("cargo:rerun-if-changed={p}");
format!("include_bytes!(r#\"{p}\"#)")
}
None => "&[]".to_string(),
};
icon_gen.push_str(&format!(
"#[allow(dead_code)] pub static {const_name}: &'static [u8] = {value};\n"
));
}
// Watch the resources dir so new/removed icon files trigger a rebuild
// (rerun-if-changed on a non-existent file is a no-op).
if let Some(dir) = resources_dir.as_ref().filter(|d| d.is_dir()) {
println!("cargo:rerun-if-changed={}", dir.display());
}
std::fs::write(Path::new(&out_dir).join("app_icon_gen.rs"), icon_gen).unwrap();
println!("cargo:rustc-check-cfg=cfg(apple_bundle,apple_sim,lines,use_gles_3,use_vulkan,linux_direct,quest,no_android_choreographer,ohos_sim,gpusim,use_unstable_unix_socket_ancillary_data_2021)");
println!("cargo:rerun-if-env-changed=MAKEPAD");
println!("cargo:rerun-if-env-changed=MAKEPAD_PACKAGE_DIR");
println!("cargo:rerun-if-env-changed=MAKEPAD_BUNDLE_NAME");
println!("cargo:rerun-if-env-changed=MAKEPAD_BUNDLE_IDENTIFIER");
println!("cargo:rerun-if-env-changed=IPHONEOS_DEPLOYMENT_TARGET");
// The GPU API on desktop Linux. The `vulkan` feature builds both
// renderers into the binary; it picks between them at startup (see
// os/linux/gpu_preference.rs), so the feature is a capability, not a
// choice of API. `MAKEPAD=gl` wins over it and produces an OpenGL-only
// binary; `MAKEPAD=vulkan` is the workspace-wide switch; the simulated
// GPU (`MAKEPAD=gpusim`) is never combined with either.
//
// The feature only reaches the windowed desktop backend, which is what
// has the runtime fallback. It must not follow from `target_os` alone:
// OpenHarmony (`*-unknown-linux-ohos`) reports Linux too, and neither it
// nor a Linux target outside the two tables that carry `naga` (see
// Cargo.toml) can compile the Vulkan renderer at all. `linux_direct`
// (DRM/KMS) can, but it drives the display exclusively and has no OpenGL
// fallback of its own, so it stays opt-in through
// `MAKEPAD=linux_direct+vulkan`.
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_VULKAN");
let makepad_configs: Vec<String> = env::var("MAKEPAD")
.map(|configs| configs.split(['+', ',']).map(str::to_string).collect())
.unwrap_or_default();
let names_gpu_api = makepad_configs
.iter()
.any(|config| matches!(config.as_str(), "gl" | "vulkan" | "use_vulkan" | "gpusim" | "quest"));
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let windowed_desktop_linux = target_os == "linux"
&& target_env == "gnu"
&& matches!(target_arch.as_str(), "x86_64" | "aarch64")
&& !makepad_configs.iter().any(|config| config == "linux_direct");
if windowed_desktop_linux && env::var("CARGO_FEATURE_VULKAN").is_ok() && !names_gpu_api {
println!("cargo:rustc-cfg=use_vulkan");
}
if let Ok(configs) = env::var("MAKEPAD") {
for config in configs.split(['+', ',']) {
match config {
"lines" => println!("cargo:rustc-cfg=lines"),
"linux_direct" => println!("cargo:rustc-cfg=linux_direct"),
"no_android_choreographer" => println!("cargo:rustc-cfg=no_android_choreographer"),
"quest" => {
println!("cargo:rustc-cfg=quest");
println!("cargo:rustc-cfg=use_gles_3");
println!("cargo:rustc-cfg=use_vulkan");
}
"apple_bundle" => println!("cargo:rustc-cfg=apple_bundle"),
"ohos_sim" => println!("cargo:rustc-cfg=ohos_sim"),
"gpusim" => println!("cargo:rustc-cfg=gpusim"),
"use_gles_3" => println!("cargo:rustc-cfg=use_gles_3"),
"vulkan" | "use_vulkan" => println!("cargo:rustc-cfg=use_vulkan"),
// The OpenGL-only build; the word overrides the `vulkan` cargo
// feature above, so an app that enables it can still be built
// without the Vulkan renderer.
"gl" => {}
_ => {}
}
}
}
match target_os.as_str() {
"macos" => {
println!("cargo:rustc-link-lib=framework=GameController");
println!("cargo:rustc-link-lib=framework=CoreHaptics");
println!("cargo:rustc-link-lib=framework=CoreLocation");
println!("cargo:rustc-link-lib=framework=AudioToolbox");
}
"ios" => {
if target == "aarch64-apple-ios-sim" {
println!("cargo:rustc-cfg=apple_sim");
}
println!("cargo:rustc-link-lib=framework=MetalKit");
println!("cargo:rustc-link-lib=framework=GameController");
println!("cargo:rustc-link-lib=framework=CoreLocation");
println!("cargo:rustc-link-lib=framework=AudioToolbox");
}
"tvos" => {
if target == "aarch64-apple-tvos-sim" {
println!("cargo:rustc-cfg=apple_sim");
}
println!("cargo:rustc-link-lib=framework=MetalKit");
println!("cargo:rustc-link-lib=framework=GameController");
}
"linux" => {
println!("cargo:rustc-cfg=use_gles_3");
println!("cargo:rustc-link-lib=xkbcommon");
}
"android" => {
println!("cargo:rustc-cfg=use_gles_3");
}
_ => (),
}
}