build.rs

  1#![cfg_attr(any(not(target_os = "macos"), feature = "macos-blade"), allow(unused))]
  2
  3//TODO: consider generating shader code for WGSL
  4//TODO: deprecate "runtime-shaders" and "macos-blade"
  5
  6use std::env;
  7
  8fn main() {
  9    let target = env::var("CARGO_CFG_TARGET_OS");
 10    println!("cargo::rustc-check-cfg=cfg(gles)");
 11
 12    #[cfg(any(
 13        not(any(target_os = "macos", target_os = "windows")),
 14        feature = "macos-blade"
 15    ))]
 16    check_wgsl_shaders();
 17
 18    match target.as_deref() {
 19        Ok("macos") => {
 20            #[cfg(target_os = "macos")]
 21            macos::build();
 22        }
 23        Ok("windows") => {
 24            #[cfg(target_os = "windows")]
 25            windows::build();
 26        }
 27        _ => (),
 28    };
 29}
 30
 31#[allow(dead_code)]
 32fn check_wgsl_shaders() {
 33    use std::path::PathBuf;
 34    use std::process;
 35    use std::str::FromStr;
 36
 37    let shader_source_path = "./src/platform/blade/shaders.wgsl";
 38    let shader_path = PathBuf::from_str(shader_source_path).unwrap();
 39    println!("cargo:rerun-if-changed={}", &shader_path.display());
 40
 41    let shader_source = std::fs::read_to_string(&shader_path).unwrap();
 42
 43    match naga::front::wgsl::parse_str(&shader_source) {
 44        Ok(_) => {
 45            // All clear
 46        }
 47        Err(e) => {
 48            eprintln!("WGSL shader compilation failed:\n{}", e);
 49            process::exit(1);
 50        }
 51    }
 52}
 53#[cfg(target_os = "macos")]
 54mod macos {
 55    use std::{
 56        env,
 57        path::{Path, PathBuf},
 58    };
 59
 60    use cbindgen::Config;
 61
 62    pub(super) fn build() {
 63        generate_dispatch_bindings();
 64        #[cfg(not(feature = "macos-blade"))]
 65        {
 66            let header_path = generate_shader_bindings();
 67
 68            #[cfg(feature = "runtime_shaders")]
 69            emit_stitched_shaders(&header_path);
 70            #[cfg(not(feature = "runtime_shaders"))]
 71            compile_metal_shaders(&header_path);
 72        }
 73    }
 74
 75    fn generate_dispatch_bindings() {
 76        println!("cargo:rustc-link-lib=framework=System");
 77
 78        let bindings = bindgen::Builder::default()
 79            .header("src/platform/mac/dispatch.h")
 80            .allowlist_var("_dispatch_main_q")
 81            .allowlist_var("_dispatch_source_type_data_add")
 82            .allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH")
 83            .allowlist_var("DISPATCH_TIME_NOW")
 84            .allowlist_function("dispatch_get_global_queue")
 85            .allowlist_function("dispatch_async_f")
 86            .allowlist_function("dispatch_after_f")
 87            .allowlist_function("dispatch_time")
 88            .allowlist_function("dispatch_source_merge_data")
 89            .allowlist_function("dispatch_source_create")
 90            .allowlist_function("dispatch_source_set_event_handler_f")
 91            .allowlist_function("dispatch_resume")
 92            .allowlist_function("dispatch_suspend")
 93            .allowlist_function("dispatch_source_cancel")
 94            .allowlist_function("dispatch_set_context")
 95            .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
 96            .layout_tests(false)
 97            .generate()
 98            .expect("unable to generate bindings");
 99
100        let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
101        bindings
102            .write_to_file(out_path.join("dispatch_sys.rs"))
103            .expect("couldn't write dispatch bindings");
104    }
105
106    fn generate_shader_bindings() -> PathBuf {
107        let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
108        let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
109        let mut config = Config {
110            include_guard: Some("SCENE_H".into()),
111            language: cbindgen::Language::C,
112            no_includes: true,
113            ..Default::default()
114        };
115        config.export.include.extend([
116            "Bounds".into(),
117            "Corners".into(),
118            "Edges".into(),
119            "Size".into(),
120            "Pixels".into(),
121            "PointF".into(),
122            "Hsla".into(),
123            "ContentMask".into(),
124            "Uniforms".into(),
125            "AtlasTile".into(),
126            "PathRasterizationInputIndex".into(),
127            "PathVertex_ScaledPixels".into(),
128            "PathRasterizationVertex".into(),
129            "ShadowInputIndex".into(),
130            "Shadow".into(),
131            "QuadInputIndex".into(),
132            "Underline".into(),
133            "UnderlineInputIndex".into(),
134            "Quad".into(),
135            "BorderStyle".into(),
136            "SpriteInputIndex".into(),
137            "MonochromeSprite".into(),
138            "PolychromeSprite".into(),
139            "PathSprite".into(),
140            "SurfaceInputIndex".into(),
141            "SurfaceBounds".into(),
142            "TransformationMatrix".into(),
143        ]);
144        config.no_includes = true;
145        config.enumeration.prefix_with_name = true;
146
147        let mut builder = cbindgen::Builder::new();
148
149        let src_paths = [
150            crate_dir.join("src/scene.rs"),
151            crate_dir.join("src/geometry.rs"),
152            crate_dir.join("src/color.rs"),
153            crate_dir.join("src/window.rs"),
154            crate_dir.join("src/platform.rs"),
155            crate_dir.join("src/platform/mac/metal_renderer.rs"),
156        ];
157        for src_path in src_paths {
158            println!("cargo:rerun-if-changed={}", src_path.display());
159            builder = builder.with_src(src_path);
160        }
161
162        builder
163            .with_config(config)
164            .generate()
165            .expect("Unable to generate bindings")
166            .write_to_file(&output_path);
167
168        output_path
169    }
170
171    /// To enable runtime compilation, we need to "stitch" the shaders file with the generated header
172    /// so that it is self-contained.
173    #[cfg(feature = "runtime_shaders")]
174    fn emit_stitched_shaders(header_path: &Path) {
175        use std::str::FromStr;
176        fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result<PathBuf> {
177            let header_contents = std::fs::read_to_string(header)?;
178            let shader_contents = std::fs::read_to_string(shader_path)?;
179            let stitched_contents = format!("{header_contents}\n{shader_contents}");
180            let out_path =
181                PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal");
182            std::fs::write(&out_path, stitched_contents)?;
183            Ok(out_path)
184        }
185        let shader_source_path = "./src/platform/mac/shaders.metal";
186        let shader_path = PathBuf::from_str(shader_source_path).unwrap();
187        stitch_header(header_path, &shader_path).unwrap();
188        println!("cargo:rerun-if-changed={}", &shader_source_path);
189    }
190
191    #[cfg(not(feature = "runtime_shaders"))]
192    fn compile_metal_shaders(header_path: &Path) {
193        use std::process::{self, Command};
194        let shader_path = "./src/platform/mac/shaders.metal";
195        let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
196        let metallib_output_path =
197            PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
198        println!("cargo:rerun-if-changed={}", shader_path);
199
200        let output = Command::new("xcrun")
201            .args([
202                "-sdk",
203                "macosx",
204                "metal",
205                "-gline-tables-only",
206                "-mmacosx-version-min=10.15.7",
207                "-MO",
208                "-c",
209                shader_path,
210                "-include",
211                (header_path.to_str().unwrap()),
212                "-o",
213            ])
214            .arg(&air_output_path)
215            .output()
216            .unwrap();
217
218        if !output.status.success() {
219            eprintln!(
220                "metal shader compilation failed:\n{}",
221                String::from_utf8_lossy(&output.stderr)
222            );
223            process::exit(1);
224        }
225
226        let output = Command::new("xcrun")
227            .args(["-sdk", "macosx", "metallib"])
228            .arg(air_output_path)
229            .arg("-o")
230            .arg(metallib_output_path)
231            .output()
232            .unwrap();
233
234        if !output.status.success() {
235            eprintln!(
236                "metallib compilation failed:\n{}",
237                String::from_utf8_lossy(&output.stderr)
238            );
239            process::exit(1);
240        }
241    }
242}
243
244#[cfg(target_os = "windows")]
245mod windows {
246    use std::{
247        io::Write,
248        path::{Path, PathBuf},
249        process::{self, Command},
250    };
251
252    pub(super) fn build() {
253        // Link the AMD AGS library
254        link_amd_ags();
255
256        // Compile HLSL shaders
257        compile_shaders();
258
259        // Embed the Windows manifest and resource file
260        #[cfg(feature = "windows-manifest")]
261        embed_resource();
262    }
263
264    fn link_amd_ags() {
265        // We can not use relative paths in `cargo:rustc-link-search`, so we need to use the absolute path.
266        // See: https://stackoverflow.com/questions/41917096/how-do-i-make-rustc-link-search-relative-to-the-project-location
267        let lib_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("libs");
268        #[cfg(target_pointer_width = "64")]
269        let lib_name = "amd_ags_x64_2022_MT";
270        #[cfg(target_pointer_width = "32")]
271        let lib_name = "amd_ags_x86_2022_MT";
272        println!("cargo:rustc-link-lib=static={}", lib_name);
273        println!("cargo:rustc-link-search=native={}", lib_dir.display());
274        println!(
275            "cargo:rerun-if-changed={}/{}.lib",
276            lib_dir.display(),
277            lib_name
278        );
279    }
280
281    #[cfg(feature = "windows-manifest")]
282    fn embed_resource() {
283        let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
284        let rc_file = std::path::Path::new("resources/windows/gpui.rc");
285        println!("cargo:rerun-if-changed={}", manifest.display());
286        println!("cargo:rerun-if-changed={}", rc_file.display());
287        embed_resource::compile(rc_file, embed_resource::NONE)
288            .manifest_required()
289            .unwrap();
290    }
291
292    fn compile_shaders() {
293        use std::fs;
294        use std::process::{self, Command};
295
296        let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
297            .join("src/platform/windows/shaders.hlsl");
298        let out_dir = std::env::var("OUT_DIR").unwrap();
299
300        println!("cargo:rerun-if-changed={}", shader_path.display());
301
302        // Check if fxc.exe is available
303        let fxc_path = find_fxc_compiler();
304
305        // Define all modules
306        let modules = [
307            "quad",
308            "shadow",
309            "paths",
310            "underline",
311            "monochrome_sprite",
312            "polychrome_sprite",
313        ];
314
315        let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
316        if Path::new(&rust_binding_path).exists() {
317            fs::remove_file(&rust_binding_path)
318                .expect("Failed to remove existing Rust binding file");
319        }
320        for module in &modules {
321            compile_shader_for_module(
322                module,
323                &out_dir,
324                &fxc_path,
325                shader_path.to_str().unwrap(),
326                &rust_binding_path,
327            );
328        }
329        println!(
330            "cargo:warning=Successfully compiled shaders. Output written to: {}",
331            rust_binding_path
332        );
333    }
334
335    fn find_fxc_compiler() -> String {
336        // Try to find in PATH
337        if let Ok(output) = std::process::Command::new("where").arg("fxc.exe").output() {
338            if output.status.success() {
339                let path = String::from_utf8_lossy(&output.stdout);
340                return path.trim().to_string();
341            }
342        }
343
344        // Check the default path
345        if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
346            .exists()
347        {
348            return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
349                .to_string();
350        }
351
352        // Check environment variable
353        if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
354            if Path::new(&path).exists() {
355                return path;
356            }
357        }
358
359        panic!("Failed to find fxc.exe");
360    }
361
362    fn compile_shader_for_module(
363        module: &str,
364        out_dir: &str,
365        fxc_path: &str,
366        shader_path: &str,
367        rust_binding_path: &str,
368    ) {
369        // Compile vertex shader
370        let output_file = format!("{}/{}_vs.h", out_dir, module);
371        let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
372        compile_shader_impl(
373            fxc_path,
374            &format!("{module}_vertex"),
375            &output_file,
376            &const_name,
377            shader_path,
378            "vs_5_0",
379        );
380        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
381
382        // Compile fragment shader
383        let output_file = format!("{}/{}_ps.h", out_dir, module);
384        let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
385        compile_shader_impl(
386            fxc_path,
387            &format!("{module}_fragment"),
388            &output_file,
389            &const_name,
390            shader_path,
391            "ps_5_0",
392        );
393        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
394    }
395
396    fn compile_shader_impl(
397        fxc_path: &str,
398        entry_point: &str,
399        output_path: &str,
400        var_name: &str,
401        shader_path: &str,
402        target: &str,
403    ) {
404        let output = Command::new(fxc_path)
405            .args([
406                "/T",
407                target,
408                "/E",
409                entry_point,
410                "/Fh",
411                output_path,
412                "/Vn",
413                var_name,
414                shader_path,
415            ])
416            .output();
417
418        match output {
419            Ok(result) => {
420                if result.status.success() {
421                    return;
422                }
423                eprintln!(
424                    "Pixel shader compilation failed for {}:\n{}",
425                    entry_point,
426                    String::from_utf8_lossy(&result.stderr)
427                );
428                process::exit(1);
429            }
430            Err(e) => {
431                eprintln!("Failed to run fxc for {}: {}", entry_point, e);
432                process::exit(1);
433            }
434        }
435    }
436
437    fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
438        let header_content =
439            std::fs::read_to_string(head_file).expect("Failed to read header file");
440        let const_definition = {
441            let global_var_start = header_content.find("const BYTE").unwrap();
442            let global_var = &header_content[global_var_start..];
443            let equal = global_var.find('=').unwrap();
444            global_var[equal + 1..].trim()
445        };
446        let rust_binding = format!(
447            "const {}: &[u8] = &{}\n",
448            const_name,
449            const_definition.replace('{', "[").replace('}', "]")
450        );
451        let mut options = std::fs::OpenOptions::new()
452            .write(true)
453            .create(true)
454            .append(true)
455            .open(output_path)
456            .expect("Failed to open Rust binding file");
457        options
458            .write_all(rust_binding.as_bytes())
459            .expect("Failed to write Rust binding file");
460    }
461}