浏览代码

Merge pull request #1694 from dsnopek/4.2-cherrypicks-9

Cherry-picks for the godot-cpp 4.2 branch - 9th batch
David Snopek 7 月之前
父节点
当前提交
4bc6e67d51

+ 14 - 11
.github/actions/godot-cache-restore/action.yml

@@ -3,19 +3,22 @@ description: Restore Godot build cache.
 inputs:
   cache-name:
     description: The cache base name (job name by default).
-    default: "${{github.job}}"
+    default: ${{ github.job }}
   scons-cache:
-    description: The scons cache path.
-    default: "${{github.workspace}}/.scons-cache/"
+    description: The SCons cache path.
+    default: ${{ github.workspace }}/.scons-cache/
+
 runs:
-  using: "composite"
+  using: composite
   steps:
-    - name: Restore .scons_cache directory
-      uses: actions/cache/restore@v3
+    - name: Restore SCons cache directory
+      uses: actions/cache/restore@v4
       with:
-        path: ${{inputs.scons-cache}}
-        key: ${{inputs.cache-name}}-${{env.GODOT_BASE_BRANCH}}-${{github.ref}}-${{github.sha}}
+        path: ${{ inputs.scons-cache }}
+        key: ${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}-${{ github.ref }}-${{ github.sha }}
+
         restore-keys: |
-          ${{inputs.cache-name}}-${{env.GODOT_BASE_BRANCH}}-${{github.ref}}-${{github.sha}}
-          ${{inputs.cache-name}}-${{env.GODOT_BASE_BRANCH}}-${{github.ref}}
-          ${{inputs.cache-name}}-${{env.GODOT_BASE_BRANCH}}
+          ${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}-${{ github.ref }}-${{ github.sha }}
+          ${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}-${{ github.ref }}
+          ${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}-refs/heads/${{ env.GODOT_BASE_BRANCH }}
+          ${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}

+ 6 - 5
.github/actions/godot-cache-save/action.yml

@@ -3,15 +3,16 @@ description: Save Godot build cache.
 inputs:
   cache-name:
     description: The cache base name (job name by default).
-    default: "${{github.job}}"
+    default: ${{ github.job }}
   scons-cache:
     description: The SCons cache path.
-    default: "${{github.workspace}}/.scons-cache/"
+    default: ${{ github.workspace }}/.scons-cache/
+
 runs:
-  using: "composite"
+  using: composite
   steps:
     - name: Save SCons cache directory
       uses: actions/cache/save@v4
       with:
-        path: ${{inputs.scons-cache}}
-        key: ${{inputs.cache-name}}-${{env.GODOT_BASE_BRANCH}}-${{github.ref}}-${{github.sha}}
+        path: ${{ inputs.scons-cache }}
+        key: ${{ inputs.cache-name }}-${{ env.GODOT_BASE_BRANCH }}-${{ github.ref }}-${{ github.sha }}

+ 62 - 0
.github/actions/setup-godot-cpp/action.yml

@@ -0,0 +1,62 @@
+name: Setup godot-cpp
+description: Setup build dependencies for godot-cpp.
+
+inputs:
+  platform:
+    required: true
+    description: Target platform.
+  em-version:
+    default: 3.1.62
+    description: Emscripten version.
+  windows-compiler:
+    required: true
+    description: The compiler toolchain to use on Windows ('mingw' or 'msvc').
+    type: choice
+    options:
+      - mingw
+      - msvc
+    default: mingw
+  mingw-version:
+    default: 12.2.0
+    description: MinGW version.
+  ndk-version:
+    default: r23c
+    description: Android NDK version.
+  scons-version:
+    default: 4.4.0
+    description: SCons version.
+
+runs:
+  using: composite
+  steps:
+    - name: Setup Python (for SCons)
+      uses: actions/setup-python@v5
+      with:
+        python-version: 3.x
+
+    - name: Setup Android dependencies
+      if: inputs.platform == 'android'
+      uses: nttld/setup-ndk@v1
+      with:
+        ndk-version: ${{ inputs.ndk-version }}
+        link-to-sdk: true
+
+    - name: Setup Web dependencies
+      if: inputs.platform == 'web'
+      uses: mymindstorm/setup-emsdk@v14
+      with:
+        version: ${{ inputs.em-version }}
+        no-cache: true
+
+    - name: Setup MinGW for Windows/MinGW build
+      if: inputs.platform == 'windows' && inputs.windows-compiler == 'mingw'
+      uses: egor-tensin/setup-mingw@v2
+      with:
+        version: ${{ inputs.mingw-version }}
+
+    - name: Setup SCons
+      shell: bash
+      run: |
+        python -c "import sys; print(sys.version)"
+        python -m pip install scons==${{ inputs.scons-version }}
+        scons --version

+ 11 - 34
.github/workflows/ci.yml

@@ -1,5 +1,6 @@
 name: Continuous integration
-on: [push, pull_request]
+on:
+  workflow_call:
 
 env:
   # Only used for the cache key. Increment version to force clean build.
@@ -8,7 +9,7 @@ env:
   GODOT_TEST_VERSION: 4.2.2-stable
 
 concurrency:
-  group: ci-${{github.actor}}-${{github.head_ref || github.run_number}}-${{github.ref}}
+  group: ci-${{ github.actor }}-${{ github.head_ref || github.run_number }}-${{ github.ref }}
   cancel-in-progress: true
 
 jobs:
@@ -91,7 +92,6 @@ jobs:
     env:
       SCONS_CACHE: ${{ github.workspace }}/.scons-cache/
       EM_VERSION: 3.1.39
-      EM_CACHE_FOLDER: "emsdk-cache"
 
     steps:
       - name: Checkout
@@ -105,34 +105,11 @@ jobs:
           cache-name: ${{ matrix.cache-name }}
         continue-on-error: true
 
-      - name: Set up Python (for SCons)
-        uses: actions/setup-python@v5
+      - name: Setup godot-cpp
+        uses: ./.github/actions/setup-godot-cpp
         with:
-          python-version: '3.x'
-
-      - name: Android dependencies
-        if: ${{ matrix.platform == 'android' }}
-        uses: nttld/setup-ndk@v1
-        with:
-          ndk-version: r23c
-          link-to-sdk: true
-
-      - name: Web dependencies
-        if: ${{ matrix.platform == 'web' }}
-        uses: mymindstorm/setup-emsdk@v14
-        with:
-          version: ${{env.EM_VERSION}}
-          actions-cache-folder: ${{env.EM_CACHE_FOLDER}}
-
-      - name: Setup MinGW for Windows/MinGW build
-        if: ${{ matrix.platform == 'windows' && matrix.flags == 'use_mingw=yes' }}
-        uses: egor-tensin/setup-mingw@v2
-        with:
-          version: 12.2.0
-
-      - name: Install scons
-        run: |
-          python -m pip install scons==4.0.0
+          platform: ${{ matrix.platform }}
+          windows-compiler: ${{ contains(matrix.flags, 'use_mingw=yes') && 'mingw' || 'msvc' }}
 
       - name: Generate godot-cpp sources only
         run: |
@@ -161,7 +138,7 @@ jobs:
 
       - name: Download latest Godot artifacts
         uses: dsnopek/action-download-artifact@1322f74e2dac9feed2ee76a32d9ae1ca3b4cf4e9
-        if: ${{ matrix.run-tests && env.GODOT_TEST_VERSION == 'master' }}
+        if: matrix.run-tests && env.GODOT_TEST_VERSION == 'master'
         with:
           repo: godotengine/godot
           branch: master
@@ -175,13 +152,13 @@ jobs:
           path: godot-artifacts
 
       - name: Prepare Godot artifacts for testing
-        if: ${{ matrix.run-tests && env.GODOT_TEST_VERSION == 'master' }}
+        if: matrix.run-tests && env.GODOT_TEST_VERSION == 'master'
         run: |
           chmod +x ./godot-artifacts/godot.linuxbsd.editor.x86_64.mono
           echo "GODOT=$(pwd)/godot-artifacts/godot.linuxbsd.editor.x86_64.mono" >> $GITHUB_ENV
 
       - name: Download requested Godot version for testing
-        if: ${{ matrix.run-tests && env.GODOT_TEST_VERSION != 'master' }}
+        if: matrix.run-tests && env.GODOT_TEST_VERSION != 'master'
         run: |
           wget "https://github.com/godotengine/godot-builds/releases/download/${GODOT_TEST_VERSION}/Godot_v${GODOT_TEST_VERSION}_linux.x86_64.zip" -O Godot.zip
           unzip -a Godot.zip
@@ -189,7 +166,7 @@ jobs:
           echo "GODOT=$(pwd)/Godot_v${GODOT_TEST_VERSION}_linux.x86_64" >> $GITHUB_ENV
 
       - name: Run tests
-        if: ${{ matrix.run-tests }}
+        if: matrix.run-tests
         run: |
           $GODOT --headless --version
           cd test

+ 21 - 0
.github/workflows/runner.yml

@@ -0,0 +1,21 @@
+name: 🔗 GHA
+on: [push, pull_request, merge_group]
+
+concurrency:
+  group: ci-${{ github.actor }}-${{ github.head_ref || github.run_number }}-${{ github.ref }}-runner
+  cancel-in-progress: true
+
+jobs:
+  # First stage: Only static checks, fast and prevent expensive builds from running.
+
+  static-checks:
+    if: '!vars.DISABLE_GODOT_CI'
+    name: 📊 Static Checks
+    uses: ./.github/workflows/static_checks.yml
+
+  # Second stage: Run all the builds and some of the tests.
+
+  ci:
+    name: 🛠️ Continuous Integration
+    needs: static-checks
+    uses: ./.github/workflows/ci.yml

+ 7 - 2
.github/workflows/static_checks.yml

@@ -1,8 +1,9 @@
 name: 📊 Static Checks
-on: [push, pull_request]
+on:
+  workflow_call:
 
 concurrency:
-  group: ci-${{github.actor}}-${{github.head_ref || github.run_number}}-${{github.ref}}-static
+  group: ci-${{ github.actor }}-${{ github.head_ref || github.run_number }}-${{ github.ref }}-static
   cancel-in-progress: true
 
 jobs:
@@ -31,3 +32,7 @@ jobs:
         uses: pre-commit/[email protected]
         with:
           extra_args: --verbose --hook-stage manual --files ${{ env.CHANGED_FILES }}
+
+      - name: Check generated files consistency
+        run:
+          python misc/scripts/check_get_file_list.py

+ 1 - 2
README.md

@@ -69,8 +69,7 @@ wish to help out, ensure you have an account on GitHub and create a "fork" of
 this repository. See [Pull request workflow](https://docs.godotengine.org/en/stable/community/contributing/pr_workflow.html)
 for instructions.
 
-Please install clang-format and copy the files in `misc/hooks` into `.git/hooks`
-so formatting is done before your changes are submitted.
+Please install clang-format and the [pre-commit](https://pre-commit.com/) Python framework so formatting is done before your changes are submitted. See the [code style guidelines](https://docs.godotengine.org/en/latest/contributing/development/code_style_guidelines.html#pre-commit-hook) for instructions.
 
 ## Getting started
 

+ 18 - 134
binding_generator.py

@@ -72,11 +72,14 @@ def generate_wrappers(target):
 
 def get_file_list(api_filepath, output_dir, headers=False, sources=False, profile_filepath=""):
     api = {}
-    files = []
     with open(api_filepath, encoding="utf-8") as api_file:
         api = json.load(api_file)
 
-    build_profile = parse_build_profile(profile_filepath, api)
+    return _get_file_list(api, output_dir, headers, sources)
+
+
+def _get_file_list(api, output_dir, headers=False, sources=False):
+    files = []
 
     core_gen_folder = Path(output_dir) / "gen" / "include" / "godot_cpp" / "core"
     include_gen_folder = Path(output_dir) / "gen" / "include" / "godot_cpp"
@@ -107,7 +110,7 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil
         source_filename = source_gen_folder / "classes" / (camel_to_snake(engine_class["name"]) + ".cpp")
         if headers:
             files.append(str(header_filename.as_posix()))
-        if sources and is_class_included(engine_class["name"], build_profile):
+        if sources:
             files.append(str(source_filename.as_posix()))
 
     for native_struct in api["native_structures"]:
@@ -139,128 +142,19 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False, profil
     return files
 
 
-def print_file_list(api_filepath, output_dir, headers=False, sources=False, profile_filepath=""):
-    print(*get_file_list(api_filepath, output_dir, headers, sources, profile_filepath), sep=";", end=None)
-
-
-def parse_build_profile(profile_filepath, api):
-    if profile_filepath == "":
-        return {}
-    print("Using feature build profile: " + profile_filepath)
-
-    with open(profile_filepath, encoding="utf-8") as profile_file:
-        profile = json.load(profile_file)
-
-    api_dict = {}
-    parents = {}
-    children = {}
-    for engine_class in api["classes"]:
-        api_dict[engine_class["name"]] = engine_class
-        parent = engine_class.get("inherits", "")
-        child = engine_class["name"]
-        parents[child] = parent
-        if parent == "":
-            continue
-        children[parent] = children.get(parent, [])
-        children[parent].append(child)
-
-    # Parse methods dependencies
-    deps = {}
-    reverse_deps = {}
-    for name, engine_class in api_dict.items():
-        ref_cls = set()
-        for method in engine_class.get("methods", []):
-            rtype = method.get("return_value", {}).get("type", "")
-            args = [a["type"] for a in method.get("arguments", [])]
-            if rtype in api_dict:
-                ref_cls.add(rtype)
-            elif is_enum(rtype) and get_enum_class(rtype) in api_dict:
-                ref_cls.add(get_enum_class(rtype))
-            for arg in args:
-                if arg in api_dict:
-                    ref_cls.add(arg)
-                elif is_enum(arg) and get_enum_class(arg) in api_dict:
-                    ref_cls.add(get_enum_class(arg))
-        deps[engine_class["name"]] = set(filter(lambda x: x != name, ref_cls))
-        for acls in ref_cls:
-            if acls == name:
-                continue
-            reverse_deps[acls] = reverse_deps.get(acls, set())
-            reverse_deps[acls].add(name)
-
-    included = []
-    front = list(profile.get("enabled_classes", []))
-    if front:
-        # These must always be included
-        front.append("WorkerThreadPool")
-        front.append("ClassDB")
-        front.append("ClassDBSingleton")
-    while front:
-        cls = front.pop()
-        if cls in included:
-            continue
-        included.append(cls)
-        parent = parents.get(cls, "")
-        if parent:
-            front.append(parent)
-        for rcls in deps.get(cls, set()):
-            if rcls in included or rcls in front:
-                continue
-            front.append(rcls)
-
-    excluded = []
-    front = list(profile.get("disabled_classes", []))
-    while front:
-        cls = front.pop()
-        if cls in excluded:
-            continue
-        excluded.append(cls)
-        front += children.get(cls, [])
-        for rcls in reverse_deps.get(cls, set()):
-            if rcls in excluded or rcls in front:
-                continue
-            front.append(rcls)
-
-    if included and excluded:
-        print(
-            "WARNING: Cannot specify both 'enabled_classes' and 'disabled_classes' in build profile. 'disabled_classes' will be ignored."
-        )
-
-    return {
-        "enabled_classes": included,
-        "disabled_classes": excluded,
-    }
-
-
-def scons_emit_files(target, source, env):
-    profile_filepath = env.get("build_profile", "")
-    if profile_filepath and not Path(profile_filepath).is_absolute():
-        profile_filepath = str((Path(env.Dir("#").abspath) / profile_filepath).as_posix())
-
-    files = [env.File(f) for f in get_file_list(str(source[0]), target[0].abspath, True, True, profile_filepath)]
-    env.Clean(target, files)
-    env["godot_cpp_gen_dir"] = target[0].abspath
-    return files, source
-
-
-def scons_generate_bindings(target, source, env):
-    generate_bindings(
-        str(source[0]),
-        env["generate_template_get_node"],
-        "32" if "32" in env["arch"] else "64",
-        env["precision"],
-        env["godot_cpp_gen_dir"],
-    )
-    return None
+def print_file_list(api_filepath, output_dir, headers=False, sources=False):
+    print(*get_file_list(api_filepath, output_dir, headers, sources), sep=";", end=None)
 
 
 def generate_bindings(api_filepath, use_template_get_node, bits="64", precision="single", output_dir="."):
-    api = None
-
-    target_dir = Path(output_dir) / "gen"
-
+    api = {}
     with open(api_filepath, encoding="utf-8") as api_file:
         api = json.load(api_file)
+    _generate_bindings(api, use_template_get_node, bits, precision, output_dir)
+
+
+def _generate_bindings(api, use_template_get_node, bits="64", precision="single", output_dir="."):
+    target_dir = Path(output_dir) / "gen"
 
     shutil.rmtree(target_dir, ignore_errors=True)
     target_dir.mkdir(parents=True)
@@ -2250,6 +2144,10 @@ def get_encoded_arg(arg_name, type_name, type_meta):
         result.append(f"\t{get_gdextension_type(arg_type)} {name}_encoded;")
         result.append(f"\tPtrToArg<{correct_type(type_name)}>::encode({name}, &{name}_encoded);")
         name = f"&{name}_encoded"
+    elif is_enum(type_name) and not is_bitfield(type_name):
+        result.append(f"\tint64_t {name}_encoded;")
+        result.append(f"\tPtrToArg<int64_t>::encode({name}, &{name}_encoded);")
+        name = f"&{name}_encoded"
     elif is_engine_class(type_name):
         # `{name}` is a C++ wrapper, it contains a field which is the object's pointer Godot expects.
         # We have to check `nullptr` because when the caller sends `nullptr`, the wrapper itself will be null.
@@ -2558,20 +2456,6 @@ def is_refcounted(type_name):
     return type_name in engine_classes and engine_classes[type_name]
 
 
-def is_class_included(class_name, build_profile):
-    """
-    Check if an engine class should be included.
-    This removes classes according to a build profile of enabled or disabled classes.
-    """
-    included = build_profile.get("enabled_classes", [])
-    excluded = build_profile.get("disabled_classes", [])
-    if included:
-        return class_name in included
-    if excluded:
-        return class_name not in excluded
-    return True
-
-
 def is_included(type_name, current_type):
     """
     Check if a builtin type should be included.

+ 183 - 0
build_profile.py

@@ -0,0 +1,183 @@
+import json
+import sys
+
+
+def parse_build_profile(profile_filepath, api):
+    if profile_filepath == "":
+        return {}
+
+    with open(profile_filepath, encoding="utf-8") as profile_file:
+        profile = json.load(profile_file)
+
+    api_dict = {}
+    parents = {}
+    children = {}
+    for engine_class in api["classes"]:
+        api_dict[engine_class["name"]] = engine_class
+        parent = engine_class.get("inherits", "")
+        child = engine_class["name"]
+        parents[child] = parent
+        if parent == "":
+            continue
+        children[parent] = children.get(parent, [])
+        children[parent].append(child)
+
+    included = []
+    front = list(profile.get("enabled_classes", []))
+    if front:
+        # These must always be included
+        front.append("WorkerThreadPool")
+        front.append("ClassDB")
+        front.append("ClassDBSingleton")
+        # In src/classes/low_level.cpp
+        front.append("FileAccess")
+        front.append("Image")
+        front.append("XMLParser")
+        # In include/godot_cpp/templates/thread_work_pool.hpp
+        front.append("Semaphore")
+    while front:
+        cls = front.pop()
+        if cls in included:
+            continue
+        included.append(cls)
+        parent = parents.get(cls, "")
+        if parent:
+            front.append(parent)
+
+    excluded = []
+    front = list(profile.get("disabled_classes", []))
+    while front:
+        cls = front.pop()
+        if cls in excluded:
+            continue
+        excluded.append(cls)
+        front += children.get(cls, [])
+
+    if included and excluded:
+        print(
+            "WARNING: Cannot specify both 'enabled_classes' and 'disabled_classes' in build profile. 'disabled_classes' will be ignored."
+        )
+
+    return {
+        "enabled_classes": included,
+        "disabled_classes": excluded,
+    }
+
+
+def generate_trimmed_api(source_api_filepath, profile_filepath):
+    with open(source_api_filepath, encoding="utf-8") as api_file:
+        api = json.load(api_file)
+
+    if profile_filepath == "":
+        return api
+
+    build_profile = parse_build_profile(profile_filepath, api)
+
+    engine_classes = {}
+    for class_api in api["classes"]:
+        engine_classes[class_api["name"]] = class_api["is_refcounted"]
+    for native_struct in api["native_structures"]:
+        if native_struct["name"] == "ObjectID":
+            continue
+        engine_classes[native_struct["name"]] = False
+
+    classes = []
+    for class_api in api["classes"]:
+        if not is_class_included(class_api["name"], build_profile):
+            continue
+        if "methods" in class_api:
+            methods = []
+            for method in class_api["methods"]:
+                if not is_method_included(method, build_profile, engine_classes):
+                    continue
+                methods.append(method)
+            class_api["methods"] = methods
+        classes.append(class_api)
+    api["classes"] = classes
+
+    return api
+
+
+def is_class_included(class_name, build_profile):
+    """
+    Check if an engine class should be included.
+    This removes classes according to a build profile of enabled or disabled classes.
+    """
+    included = build_profile.get("enabled_classes", [])
+    excluded = build_profile.get("disabled_classes", [])
+    if included:
+        return class_name in included
+    if excluded:
+        return class_name not in excluded
+    return True
+
+
+def is_method_included(method, build_profile, engine_classes):
+    """
+    Check if an engine class method should be included.
+    This removes methods according to a build profile of enabled or disabled classes.
+    """
+    included = build_profile.get("enabled_classes", [])
+    excluded = build_profile.get("disabled_classes", [])
+    ref_cls = set()
+    rtype = get_base_type(method.get("return_value", {}).get("type", ""))
+    args = [get_base_type(a["type"]) for a in method.get("arguments", [])]
+    if rtype in engine_classes:
+        ref_cls.add(rtype)
+    elif is_enum(rtype) and get_enum_class(rtype) in engine_classes:
+        ref_cls.add(get_enum_class(rtype))
+    for arg in args:
+        if arg in engine_classes:
+            ref_cls.add(arg)
+        elif is_enum(arg) and get_enum_class(arg) in engine_classes:
+            ref_cls.add(get_enum_class(arg))
+    for acls in ref_cls:
+        if len(included) > 0 and acls not in included:
+            return False
+        elif len(excluded) > 0 and acls in excluded:
+            return False
+    return True
+
+
+def is_enum(type_name):
+    return type_name.startswith("enum::") or type_name.startswith("bitfield::")
+
+
+def get_enum_class(enum_name: str):
+    if "." in enum_name:
+        if is_bitfield(enum_name):
+            return enum_name.replace("bitfield::", "").split(".")[0]
+        else:
+            return enum_name.replace("enum::", "").split(".")[0]
+    else:
+        return "GlobalConstants"
+
+
+def get_base_type(type_name):
+    if type_name.startswith("const "):
+        type_name = type_name[6:]
+    if type_name.endswith("*"):
+        type_name = type_name[:-1]
+    if type_name.startswith("typedarray::"):
+        type_name = type_name.replace("typedarray::", "")
+    return type_name
+
+
+def is_bitfield(type_name):
+    return type_name.startswith("bitfield::")
+
+
+if __name__ == "__main__":
+    if len(sys.argv) < 3 or len(sys.argv) > 4:
+        print("Usage: %s BUILD_PROFILE INPUT_JSON [OUTPUT_JSON]" % (sys.argv[0]))
+        sys.exit(1)
+    profile = sys.argv[1]
+    infile = sys.argv[2]
+    outfile = sys.argv[3] if len(sys.argv) > 3 else ""
+    api = generate_trimmed_api(infile, profile)
+
+    if outfile:
+        with open(outfile, "w", encoding="utf-8") as f:
+            json.dump(api, f)
+    else:
+        json.dump(api, sys.stdout)

+ 6 - 2
include/godot_cpp/classes/ref.hpp

@@ -230,7 +230,9 @@ template <typename T>
 struct PtrToArg<Ref<T>> {
 	_FORCE_INLINE_ static Ref<T> convert(const void *p_ptr) {
 		GDExtensionRefPtr ref = (GDExtensionRefPtr)p_ptr;
-		ERR_FAIL_NULL_V(p_ptr, Ref<T>());
+		if (unlikely(!p_ptr)) {
+			return Ref<T>();
+		}
 		return Ref<T>(reinterpret_cast<T *>(godot::internal::get_object_instance_binding(godot::internal::gdextension_interface_ref_get_object(ref))));
 	}
 
@@ -254,7 +256,9 @@ struct PtrToArg<const Ref<T> &> {
 
 	_FORCE_INLINE_ static Ref<T> convert(const void *p_ptr) {
 		GDExtensionRefPtr ref = const_cast<GDExtensionRefPtr>(p_ptr);
-		ERR_FAIL_NULL_V(p_ptr, Ref<T>());
+		if (unlikely(!p_ptr)) {
+			return Ref<T>();
+		}
 		return Ref<T>(reinterpret_cast<T *>(godot::internal::get_object_instance_binding(godot::internal::gdextension_interface_ref_get_object(ref))));
 	}
 };

+ 8 - 0
include/godot_cpp/core/defs.hpp

@@ -35,6 +35,8 @@
 #include <cstdint>
 #include <cstring>
 
+namespace godot {
+
 #if !defined(GDE_EXPORT)
 #if defined(_WIN32)
 #define GDE_EXPORT __declspec(dllexport)
@@ -127,4 +129,10 @@ struct BuildIndexSequence : BuildIndexSequence<N - 1, N - 1, Is...> {};
 template <size_t... Is>
 struct BuildIndexSequence<0, Is...> : IndexSequence<Is...> {};
 
+} //namespace godot
+
+// To maintain compatibility an alias is defined outside the namespace.
+// Consider it deprecated.
+using real_t = godot::real_t;
+
 #endif // GODOT_DEFS_HPP

+ 42 - 44
include/godot_cpp/variant/quaternion.hpp

@@ -31,6 +31,7 @@
 #ifndef GODOT_QUATERNION_HPP
 #define GODOT_QUATERNION_HPP
 
+#include <godot_cpp/classes/global_constants.hpp>
 #include <godot_cpp/core/math.hpp>
 #include <godot_cpp/variant/vector3.hpp>
 
@@ -47,11 +48,11 @@ struct _NO_DISCARD_ Quaternion {
 		real_t components[4] = { 0, 0, 0, 1.0 };
 	};
 
-	_FORCE_INLINE_ real_t &operator[](int idx) {
-		return components[idx];
+	_FORCE_INLINE_ real_t &operator[](int p_idx) {
+		return components[p_idx];
 	}
-	_FORCE_INLINE_ const real_t &operator[](int idx) const {
-		return components[idx];
+	_FORCE_INLINE_ const real_t &operator[](int p_idx) const {
+		return components[p_idx];
 	}
 	_FORCE_INLINE_ real_t length_squared() const;
 	bool is_equal_approx(const Quaternion &p_quaternion) const;
@@ -66,14 +67,13 @@ struct _NO_DISCARD_ Quaternion {
 	_FORCE_INLINE_ real_t dot(const Quaternion &p_q) const;
 	real_t angle_to(const Quaternion &p_to) const;
 
-	Vector3 get_euler_xyz() const;
-	Vector3 get_euler_yxz() const;
-	Vector3 get_euler() const { return get_euler_yxz(); }
+	Vector3 get_euler(EulerOrder p_order = EulerOrder::EULER_ORDER_YXZ) const;
+	static Quaternion from_euler(const Vector3 &p_euler);
 
-	Quaternion slerp(const Quaternion &p_to, const real_t &p_weight) const;
-	Quaternion slerpni(const Quaternion &p_to, const real_t &p_weight) const;
-	Quaternion spherical_cubic_interpolate(const Quaternion &p_b, const Quaternion &p_pre_a, const Quaternion &p_post_b, const real_t &p_weight) const;
-	Quaternion spherical_cubic_interpolate_in_time(const Quaternion &p_b, const Quaternion &p_pre_a, const Quaternion &p_post_b, const real_t &p_weight, const real_t &p_b_t, const real_t &p_pre_a_t, const real_t &p_post_b_t) const;
+	Quaternion slerp(const Quaternion &p_to, real_t p_weight) const;
+	Quaternion slerpni(const Quaternion &p_to, real_t p_weight) const;
+	Quaternion spherical_cubic_interpolate(const Quaternion &p_b, const Quaternion &p_pre_a, const Quaternion &p_post_b, real_t p_weight) const;
+	Quaternion spherical_cubic_interpolate_in_time(const Quaternion &p_b, const Quaternion &p_pre_a, const Quaternion &p_post_b, real_t p_weight, real_t p_b_t, real_t p_pre_a_t, real_t p_post_b_t) const;
 
 	Vector3 get_axis() const;
 	real_t get_angle() const;
@@ -89,28 +89,28 @@ struct _NO_DISCARD_ Quaternion {
 	void operator*=(const Quaternion &p_q);
 	Quaternion operator*(const Quaternion &p_q) const;
 
-	_FORCE_INLINE_ Vector3 xform(const Vector3 &v) const {
+	_FORCE_INLINE_ Vector3 xform(const Vector3 &p_v) const {
 #ifdef MATH_CHECKS
-		ERR_FAIL_COND_V_MSG(!is_normalized(), v, "The quaternion must be normalized.");
+		ERR_FAIL_COND_V_MSG(!is_normalized(), p_v, "The quaternion " + operator String() + " must be normalized.");
 #endif
 		Vector3 u(x, y, z);
-		Vector3 uv = u.cross(v);
-		return v + ((uv * w) + u.cross(uv)) * ((real_t)2);
+		Vector3 uv = u.cross(p_v);
+		return p_v + ((uv * w) + u.cross(uv)) * ((real_t)2);
 	}
 
-	_FORCE_INLINE_ Vector3 xform_inv(const Vector3 &v) const {
-		return inverse().xform(v);
+	_FORCE_INLINE_ Vector3 xform_inv(const Vector3 &p_v) const {
+		return inverse().xform(p_v);
 	}
 
 	_FORCE_INLINE_ void operator+=(const Quaternion &p_q);
 	_FORCE_INLINE_ void operator-=(const Quaternion &p_q);
-	_FORCE_INLINE_ void operator*=(const real_t &s);
-	_FORCE_INLINE_ void operator/=(const real_t &s);
-	_FORCE_INLINE_ Quaternion operator+(const Quaternion &q2) const;
-	_FORCE_INLINE_ Quaternion operator-(const Quaternion &q2) const;
+	_FORCE_INLINE_ void operator*=(real_t p_s);
+	_FORCE_INLINE_ void operator/=(real_t p_s);
+	_FORCE_INLINE_ Quaternion operator+(const Quaternion &p_q2) const;
+	_FORCE_INLINE_ Quaternion operator-(const Quaternion &p_q2) const;
 	_FORCE_INLINE_ Quaternion operator-() const;
-	_FORCE_INLINE_ Quaternion operator*(const real_t &s) const;
-	_FORCE_INLINE_ Quaternion operator/(const real_t &s) const;
+	_FORCE_INLINE_ Quaternion operator*(real_t p_s) const;
+	_FORCE_INLINE_ Quaternion operator/(real_t p_s) const;
 
 	_FORCE_INLINE_ bool operator==(const Quaternion &p_quaternion) const;
 	_FORCE_INLINE_ bool operator!=(const Quaternion &p_quaternion) const;
@@ -128,8 +128,6 @@ struct _NO_DISCARD_ Quaternion {
 
 	Quaternion(const Vector3 &p_axis, real_t p_angle);
 
-	Quaternion(const Vector3 &p_euler);
-
 	Quaternion(const Quaternion &p_q) :
 			x(p_q.x),
 			y(p_q.y),
@@ -144,9 +142,9 @@ struct _NO_DISCARD_ Quaternion {
 		w = p_q.w;
 	}
 
-	Quaternion(const Vector3 &v0, const Vector3 &v1) { // Shortest arc.
-		Vector3 c = v0.cross(v1);
-		real_t d = v0.dot(v1);
+	Quaternion(const Vector3 &p_v0, const Vector3 &p_v1) { // Shortest arc.
+		Vector3 c = p_v0.cross(p_v1);
+		real_t d = p_v0.dot(p_v1);
 
 		if (d < -1.0f + (real_t)CMP_EPSILON) {
 			x = 0;
@@ -187,25 +185,25 @@ void Quaternion::operator-=(const Quaternion &p_q) {
 	w -= p_q.w;
 }
 
-void Quaternion::operator*=(const real_t &s) {
-	x *= s;
-	y *= s;
-	z *= s;
-	w *= s;
+void Quaternion::operator*=(real_t p_s) {
+	x *= p_s;
+	y *= p_s;
+	z *= p_s;
+	w *= p_s;
 }
 
-void Quaternion::operator/=(const real_t &s) {
-	*this *= 1.0f / s;
+void Quaternion::operator/=(real_t p_s) {
+	*this *= 1.0f / p_s;
 }
 
-Quaternion Quaternion::operator+(const Quaternion &q2) const {
+Quaternion Quaternion::operator+(const Quaternion &p_q2) const {
 	const Quaternion &q1 = *this;
-	return Quaternion(q1.x + q2.x, q1.y + q2.y, q1.z + q2.z, q1.w + q2.w);
+	return Quaternion(q1.x + p_q2.x, q1.y + p_q2.y, q1.z + p_q2.z, q1.w + p_q2.w);
 }
 
-Quaternion Quaternion::operator-(const Quaternion &q2) const {
+Quaternion Quaternion::operator-(const Quaternion &p_q2) const {
 	const Quaternion &q1 = *this;
-	return Quaternion(q1.x - q2.x, q1.y - q2.y, q1.z - q2.z, q1.w - q2.w);
+	return Quaternion(q1.x - p_q2.x, q1.y - p_q2.y, q1.z - p_q2.z, q1.w - p_q2.w);
 }
 
 Quaternion Quaternion::operator-() const {
@@ -213,12 +211,12 @@ Quaternion Quaternion::operator-() const {
 	return Quaternion(-q2.x, -q2.y, -q2.z, -q2.w);
 }
 
-Quaternion Quaternion::operator*(const real_t &s) const {
-	return Quaternion(x * s, y * s, z * s, w * s);
+Quaternion Quaternion::operator*(real_t p_s) const {
+	return Quaternion(x * p_s, y * p_s, z * p_s, w * p_s);
 }
 
-Quaternion Quaternion::operator/(const real_t &s) const {
-	return *this * (1.0f / s);
+Quaternion Quaternion::operator/(real_t p_s) const {
+	return *this * (1.0f / p_s);
 }
 
 bool Quaternion::operator==(const Quaternion &p_quaternion) const {
@@ -229,7 +227,7 @@ bool Quaternion::operator!=(const Quaternion &p_quaternion) const {
 	return x != p_quaternion.x || y != p_quaternion.y || z != p_quaternion.z || w != p_quaternion.w;
 }
 
-_FORCE_INLINE_ Quaternion operator*(const real_t &p_real, const Quaternion &p_quaternion) {
+_FORCE_INLINE_ Quaternion operator*(real_t p_real, const Quaternion &p_quaternion) {
 	return p_quaternion * p_real;
 }
 

+ 0 - 2
include/godot_cpp/variant/variant.hpp

@@ -324,8 +324,6 @@ public:
 	bool booleanize() const;
 	String stringify() const;
 	Variant duplicate(bool deep = false) const;
-	static void blend(const Variant &a, const Variant &b, float c, Variant &r_dst);
-	static void interpolate(const Variant &a, const Variant &b, float c, Variant &r_dst);
 
 	static String get_type_name(Variant::Type type);
 	static bool can_convert(Variant::Type from, Variant::Type to);

+ 4 - 3
include/godot_cpp/variant/vector4.hpp

@@ -55,16 +55,17 @@ struct _NO_DISCARD_ Vector4 {
 			real_t z;
 			real_t w;
 		};
-		real_t components[4] = { 0, 0, 0, 0 };
+		[[deprecated("Use coord instead")]] real_t components[4];
+		real_t coord[4] = { 0, 0, 0, 0 };
 	};
 
 	_FORCE_INLINE_ real_t &operator[](const int p_axis) {
 		DEV_ASSERT((unsigned int)p_axis < 4);
-		return components[p_axis];
+		return coord[p_axis];
 	}
 	_FORCE_INLINE_ const real_t &operator[](const int p_axis) const {
 		DEV_ASSERT((unsigned int)p_axis < 4);
-		return components[p_axis];
+		return coord[p_axis];
 	}
 
 	Vector4::Axis min_axis_index() const;

+ 31 - 17
misc/scripts/check_get_file_list.py

@@ -6,26 +6,40 @@ from pathlib import Path
 
 sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", ".."))
 
-from binding_generator import generate_bindings, get_file_list
+from binding_generator import _generate_bindings, _get_file_list
+from build_profile import generate_trimmed_api
 
 api_filepath = "gdextension/extension_api.json"
 bits = "64"
 precision = "single"
 output_dir = "self_test"
 
-generate_bindings(api_filepath, use_template_get_node=False, bits=bits, precision=precision, output_dir=output_dir)
-flist = get_file_list(api_filepath, output_dir, headers=True, sources=True)
-
-p = Path(output_dir) / "gen"
-allfiles = [str(f.as_posix()) for f in p.glob("**/*.*")]
-missing = list(filter((lambda f: f not in flist), allfiles))
-extras = list(filter((lambda f: f not in allfiles), flist))
-if len(missing) > 0 or len(extras) > 0:
-    print("Error!")
-    for f in missing:
-        print("MISSING: " + str(f))
-    for f in extras:
-        print("EXTRA: " + str(f))
-    sys.exit(1)
-else:
-    print("OK!")
+
+def test(profile_filepath=""):
+    api = generate_trimmed_api(api_filepath, profile_filepath)
+    _generate_bindings(
+        api,
+        use_template_get_node=False,
+        bits=bits,
+        precision=precision,
+        output_dir=output_dir,
+    )
+    flist = _get_file_list(api, output_dir, headers=True, sources=True)
+
+    p = Path(output_dir) / "gen"
+    allfiles = [str(f.as_posix()) for f in p.glob("**/*.*")]
+    missing = list(filter((lambda f: f not in flist), allfiles))
+    extras = list(filter((lambda f: f not in allfiles), flist))
+    if len(missing) > 0 or len(extras) > 0:
+        print("Error!")
+        for f in missing:
+            print("MISSING: " + str(f))
+        for f in extras:
+            print("EXTRA: " + str(f))
+        sys.exit(1)
+    else:
+        print("OK!")
+
+
+test()
+test("test/build_profile.json")

+ 28 - 40
src/variant/quaternion.cpp

@@ -37,28 +37,15 @@ namespace godot {
 
 real_t Quaternion::angle_to(const Quaternion &p_to) const {
 	real_t d = dot(p_to);
-	return Math::acos(CLAMP(d * d * 2 - 1, -1, 1));
+	// acos does clamping.
+	return Math::acos(d * d * 2 - 1);
 }
 
-// get_euler_xyz returns a vector containing the Euler angles in the format
-// (ax,ay,az), where ax is the angle of rotation around x axis,
-// and similar for other axes.
-// This implementation uses XYZ convention (Z is the first rotation).
-Vector3 Quaternion::get_euler_xyz() const {
-	Basis m(*this);
-	return m.get_euler(EULER_ORDER_XYZ);
-}
-
-// get_euler_yxz returns a vector containing the Euler angles in the format
-// (ax,ay,az), where ax is the angle of rotation around x axis,
-// and similar for other axes.
-// This implementation uses YXZ convention (Z is the first rotation).
-Vector3 Quaternion::get_euler_yxz() const {
+Vector3 Quaternion::get_euler(EulerOrder p_order) const {
 #ifdef MATH_CHECKS
-	ERR_FAIL_COND_V_MSG(!is_normalized(), Vector3(0, 0, 0), "The quaternion must be normalized.");
+	ERR_FAIL_COND_V_MSG(!is_normalized(), Vector3(0, 0, 0), "The quaternion " + operator String() + " must be normalized.");
 #endif
-	Basis m(*this);
-	return m.get_euler(EULER_ORDER_YXZ);
+	return Basis(*this).get_euler(p_order);
 }
 
 void Quaternion::operator*=(const Quaternion &p_q) {
@@ -103,7 +90,7 @@ bool Quaternion::is_normalized() const {
 
 Quaternion Quaternion::inverse() const {
 #ifdef MATH_CHECKS
-	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The quaternion must be normalized.");
+	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The quaternion " + operator String() + " must be normalized.");
 #endif
 	return Quaternion(-x, -y, -z, w);
 }
@@ -125,10 +112,10 @@ Quaternion Quaternion::exp() const {
 	return Quaternion(src_v, theta);
 }
 
-Quaternion Quaternion::slerp(const Quaternion &p_to, const real_t &p_weight) const {
+Quaternion Quaternion::slerp(const Quaternion &p_to, real_t p_weight) const {
 #ifdef MATH_CHECKS
-	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The start quaternion must be normalized.");
-	ERR_FAIL_COND_V_MSG(!p_to.is_normalized(), Quaternion(), "The end quaternion must be normalized.");
+	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The start quaternion " + operator String() + " must be normalized.");
+	ERR_FAIL_COND_V_MSG(!p_to.is_normalized(), Quaternion(), "The end quaternion " + p_to.operator String() + " must be normalized.");
 #endif
 	Quaternion to1;
 	real_t omega, cosom, sinom, scale0, scale1;
@@ -166,10 +153,10 @@ Quaternion Quaternion::slerp(const Quaternion &p_to, const real_t &p_weight) con
 			scale0 * w + scale1 * to1.w);
 }
 
-Quaternion Quaternion::slerpni(const Quaternion &p_to, const real_t &p_weight) const {
+Quaternion Quaternion::slerpni(const Quaternion &p_to, real_t p_weight) const {
 #ifdef MATH_CHECKS
-	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The start quaternion must be normalized.");
-	ERR_FAIL_COND_V_MSG(!p_to.is_normalized(), Quaternion(), "The end quaternion must be normalized.");
+	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The start quaternion " + operator String() + " must be normalized.");
+	ERR_FAIL_COND_V_MSG(!p_to.is_normalized(), Quaternion(), "The end quaternion " + p_to.operator String() + " must be normalized.");
 #endif
 	const Quaternion &from = *this;
 
@@ -190,10 +177,10 @@ Quaternion Quaternion::slerpni(const Quaternion &p_to, const real_t &p_weight) c
 			invFactor * from.w + newFactor * p_to.w);
 }
 
-Quaternion Quaternion::spherical_cubic_interpolate(const Quaternion &p_b, const Quaternion &p_pre_a, const Quaternion &p_post_b, const real_t &p_weight) const {
+Quaternion Quaternion::spherical_cubic_interpolate(const Quaternion &p_b, const Quaternion &p_pre_a, const Quaternion &p_post_b, real_t p_weight) const {
 #ifdef MATH_CHECKS
-	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The start quaternion must be normalized.");
-	ERR_FAIL_COND_V_MSG(!p_b.is_normalized(), Quaternion(), "The end quaternion must be normalized.");
+	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The start quaternion " + operator String() + " must be normalized.");
+	ERR_FAIL_COND_V_MSG(!p_b.is_normalized(), Quaternion(), "The end quaternion " + p_b.operator String() + " must be normalized.");
 #endif
 	Quaternion from_q = *this;
 	Quaternion pre_q = p_pre_a;
@@ -236,15 +223,15 @@ Quaternion Quaternion::spherical_cubic_interpolate(const Quaternion &p_b, const
 	ln.z = Math::cubic_interpolate(ln_from.z, ln_to.z, ln_pre.z, ln_post.z, p_weight);
 	Quaternion q2 = to_q * ln.exp();
 
-	// To cancel error made by Expmap ambiguity, do blends.
+	// To cancel error made by Expmap ambiguity, do blending.
 	return q1.slerp(q2, p_weight);
 }
 
-Quaternion Quaternion::spherical_cubic_interpolate_in_time(const Quaternion &p_b, const Quaternion &p_pre_a, const Quaternion &p_post_b, const real_t &p_weight,
-		const real_t &p_b_t, const real_t &p_pre_a_t, const real_t &p_post_b_t) const {
+Quaternion Quaternion::spherical_cubic_interpolate_in_time(const Quaternion &p_b, const Quaternion &p_pre_a, const Quaternion &p_post_b, real_t p_weight,
+		real_t p_b_t, real_t p_pre_a_t, real_t p_post_b_t) const {
 #ifdef MATH_CHECKS
-	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The start quaternion must be normalized.");
-	ERR_FAIL_COND_V_MSG(!p_b.is_normalized(), Quaternion(), "The end quaternion must be normalized.");
+	ERR_FAIL_COND_V_MSG(!is_normalized(), Quaternion(), "The start quaternion " + operator String() + " must be normalized.");
+	ERR_FAIL_COND_V_MSG(!p_b.is_normalized(), Quaternion(), "The end quaternion " + p_b.operator String() + " must be normalized.");
 #endif
 	Quaternion from_q = *this;
 	Quaternion pre_q = p_pre_a;
@@ -287,7 +274,7 @@ Quaternion Quaternion::spherical_cubic_interpolate_in_time(const Quaternion &p_b
 	ln.z = Math::cubic_interpolate_in_time(ln_from.z, ln_to.z, ln_pre.z, ln_post.z, p_weight, p_b_t, p_pre_a_t, p_post_b_t);
 	Quaternion q2 = to_q * ln.exp();
 
-	// To cancel error made by Expmap ambiguity, do blends.
+	// To cancel error made by Expmap ambiguity, do blending.
 	return q1.slerp(q2, p_weight);
 }
 
@@ -309,7 +296,7 @@ real_t Quaternion::get_angle() const {
 
 Quaternion::Quaternion(const Vector3 &p_axis, real_t p_angle) {
 #ifdef MATH_CHECKS
-	ERR_FAIL_COND_MSG(!p_axis.is_normalized(), "The axis Vector3 must be normalized.");
+	ERR_FAIL_COND_MSG(!p_axis.is_normalized(), "The axis Vector3 " + p_axis.operator String() + " must be normalized.");
 #endif
 	real_t d = p_axis.length();
 	if (d == 0) {
@@ -332,7 +319,7 @@ Quaternion::Quaternion(const Vector3 &p_axis, real_t p_angle) {
 // (ax, ay, az), where ax is the angle of rotation around x axis,
 // and similar for other axes.
 // This implementation uses YXZ convention (Z is the first rotation).
-Quaternion::Quaternion(const Vector3 &p_euler) {
+Quaternion Quaternion::from_euler(const Vector3 &p_euler) {
 	real_t half_a1 = p_euler.y * 0.5f;
 	real_t half_a2 = p_euler.x * 0.5f;
 	real_t half_a3 = p_euler.z * 0.5f;
@@ -348,10 +335,11 @@ Quaternion::Quaternion(const Vector3 &p_euler) {
 	real_t cos_a3 = Math::cos(half_a3);
 	real_t sin_a3 = Math::sin(half_a3);
 
-	x = sin_a1 * cos_a2 * sin_a3 + cos_a1 * sin_a2 * cos_a3;
-	y = sin_a1 * cos_a2 * cos_a3 - cos_a1 * sin_a2 * sin_a3;
-	z = -sin_a1 * sin_a2 * cos_a3 + cos_a1 * cos_a2 * sin_a3;
-	w = sin_a1 * sin_a2 * sin_a3 + cos_a1 * cos_a2 * cos_a3;
+	return Quaternion(
+			sin_a1 * cos_a2 * sin_a3 + cos_a1 * sin_a2 * cos_a3,
+			sin_a1 * cos_a2 * cos_a3 - cos_a1 * sin_a2 * sin_a3,
+			-sin_a1 * sin_a2 * cos_a3 + cos_a1 * cos_a2 * sin_a3,
+			sin_a1 * sin_a2 * sin_a3 + cos_a1 * cos_a2 * cos_a3);
 }
 
 } // namespace godot

+ 5 - 1
test/build_profile.json

@@ -1,9 +1,13 @@
 {
 	"enabled_classes": [
 		"Control",
+		"InputEventKey",
 		"Label",
+		"MultiplayerAPI",
+		"MultiplayerPeer",
 		"OS",
 		"TileMap",
-		"InputEventKey"
+		"TileSet",
+		"Viewport"
 	]
 }

+ 5 - 0
tools/android.py

@@ -120,4 +120,9 @@ def generate(env):
 
     env.Append(CPPDEFINES=["ANDROID_ENABLED", "UNIX_ENABLED"])
 
+    # Refer to https://github.com/godotengine/godot/blob/master/platform/android/detect.py
+    # LTO benefits for Android (size, performance) haven't been clearly established yet.
+    if env["lto"] == "auto":
+        env["lto"] = "none"
+
     common_compiler_flags.generate(env)

+ 30 - 0
tools/common_compiler_flags.py

@@ -22,6 +22,10 @@ def exists(env):
 
 
 def generate(env):
+    assert env["lto"] in ["thin", "full", "none"], "Unrecognized lto: {}".format(env["lto"])
+    if env["lto"] != "none":
+        print("Using LTO: " + env["lto"])
+
     # Require C++17
     if env.get("is_msvc", False):
         env.Append(CXXFLAGS=["/std:c++17"])
@@ -64,6 +68,22 @@ def generate(env):
             env.Append(LINKFLAGS=["/OPT:REF"])
         elif env["optimize"] == "debug" or env["optimize"] == "none":
             env.Append(CCFLAGS=["/Od"])
+
+        if env["lto"] == "thin":
+            if not env["use_llvm"]:
+                print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.")
+                env.Exit(255)
+
+            env.Append(CCFLAGS=["-flto=thin"])
+            env.Append(LINKFLAGS=["-flto=thin"])
+        elif env["lto"] == "full":
+            if env["use_llvm"]:
+                env.Append(CCFLAGS=["-flto"])
+                env.Append(LINKFLAGS=["-flto"])
+            else:
+                env.AppendUnique(CCFLAGS=["/GL"])
+                env.AppendUnique(ARFLAGS=["/LTCG"])
+                env.AppendUnique(LINKFLAGS=["/LTCG"])
     else:
         if env["debug_symbols"]:
             # Adding dwarf-4 explicitly makes stacktraces work with clang builds,
@@ -91,3 +111,13 @@ def generate(env):
             env.Append(CCFLAGS=["-Og"])
         elif env["optimize"] == "none":
             env.Append(CCFLAGS=["-O0"])
+
+        if env["lto"] == "thin":
+            if (env["platform"] == "windows" or env["platform"] == "linux") and not env["use_llvm"]:
+                print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.")
+                env.Exit(255)
+            env.Append(CCFLAGS=["-flto=thin"])
+            env.Append(LINKFLAGS=["-flto=thin"])
+        elif env["lto"] == "full":
+            env.Append(CCFLAGS=["-flto"])
+            env.Append(LINKFLAGS=["-flto"])

+ 41 - 1
tools/godotcpp.py

@@ -10,7 +10,8 @@ from SCons.Tool import Tool
 from SCons.Variables import BoolVariable, EnumVariable, PathVariable
 from SCons.Variables.BoolVariable import _text2bool
 
-from binding_generator import scons_emit_files, scons_generate_bindings
+from binding_generator import _generate_bindings, _get_file_list, get_file_list
+from build_profile import generate_trimmed_api
 
 
 def add_sources(sources, dir, extension):
@@ -129,6 +130,37 @@ def no_verbose(env):
     env.Append(GENCOMSTR=[generated_file_message])
 
 
+def scons_emit_files(target, source, env):
+    profile_filepath = env.get("build_profile", "")
+    if profile_filepath:
+        profile_filepath = normalize_path(profile_filepath, env)
+
+    # Always clean all files
+    env.Clean(target, [env.File(f) for f in get_file_list(str(source[0]), target[0].abspath, True, True)])
+
+    api = generate_trimmed_api(str(source[0]), profile_filepath)
+    files = [env.File(f) for f in _get_file_list(api, target[0].abspath, True, True)]
+    env["godot_cpp_gen_dir"] = target[0].abspath
+    return files, source
+
+
+def scons_generate_bindings(target, source, env):
+    profile_filepath = env.get("build_profile", "")
+    if profile_filepath:
+        profile_filepath = normalize_path(profile_filepath, env)
+
+    api = generate_trimmed_api(str(source[0]), profile_filepath)
+
+    _generate_bindings(
+        api,
+        env["generate_template_get_node"],
+        "32" if "32" in env["arch"] else "64",
+        env["precision"],
+        env["godot_cpp_gen_dir"],
+    )
+    return None
+
+
 platforms = ["linux", "macos", "windows", "android", "ios", "web"]
 
 # CPU architecture options.
@@ -326,6 +358,14 @@ def options(opts, env):
             ("none", "custom", "debug", "speed", "speed_trace", "size"),
         )
     )
+    opts.Add(
+        EnumVariable(
+            "lto",
+            "Link-time optimization",
+            "none",
+            ("none", "auto", "thin", "full"),
+        )
+    )
     opts.Add(BoolVariable("debug_symbols", "Build with debugging symbols", True))
     opts.Add(BoolVariable("dev_build", "Developer build with dev-only debugging code (DEV_ENABLED)", False))
     opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", False))

+ 5 - 0
tools/ios.py

@@ -97,4 +97,9 @@ def generate(env):
 
     env.Append(CPPDEFINES=["IOS_ENABLED", "UNIX_ENABLED"])
 
+    # Refer to https://github.com/godotengine/godot/blob/master/platform/ios/detect.py:
+    # Disable by default as it makes linking in Xcode very slow.
+    if env["lto"] == "auto":
+        env["lto"] = "none"
+
     common_compiler_flags.generate(env)

+ 4 - 0
tools/linux.py

@@ -39,4 +39,8 @@ def generate(env):
 
     env.Append(CPPDEFINES=["LINUX_ENABLED", "UNIX_ENABLED"])
 
+    # Refer to https://github.com/godotengine/godot/blob/master/platform/linuxbsd/detect.py
+    if env["lto"] == "auto":
+        env["lto"] = "full"
+
     common_compiler_flags.generate(env)

+ 5 - 0
tools/macos.py

@@ -73,4 +73,9 @@ def generate(env):
 
     env.Append(CPPDEFINES=["MACOS_ENABLED", "UNIX_ENABLED"])
 
+    # Refer to https://github.com/godotengine/godot/blob/master/platform/macos/detect.py
+    # LTO benefits for macOS (size, performance) haven't been clearly established yet.
+    if env["lto"] == "auto":
+        env["lto"] = "none"
+
     common_compiler_flags.generate(env)

+ 4 - 0
tools/web.py

@@ -48,4 +48,8 @@ def generate(env):
 
     env.Append(CPPDEFINES=["WEB_ENABLED", "UNIX_ENABLED"])
 
+    # Refer to https://github.com/godotengine/godot/blob/master/platform/web/detect.py
+    if env["lto"] == "auto":
+        env["lto"] = "full"
+
     common_compiler_flags.generate(env)

+ 8 - 0
tools/windows.py

@@ -198,4 +198,12 @@ def generate(env):
 
     env.Append(CPPDEFINES=["WINDOWS_ENABLED"])
 
+    # Refer to https://github.com/godotengine/godot/blob/master/platform/windows/detect.py
+    if env["lto"] == "auto":
+        if env.get("is_msvc", False):
+            # No LTO by default for MSVC, doesn't help.
+            env["lto"] = "none"
+        else:  # Release
+            env["lto"] = "full"
+
     common_compiler_flags.generate(env)