Godot 4.7 Render Architecture: Theoretical and Technical Analysis of CPU Bottlenecks
General Architectural Framework and CPU-GPU Synchronization Points
The modern render architecture of the Godot game engine, especially in versions supporting 3D and advanced 2D graphics, relies on a complex layer structure to ensure platform independence and access to low-level graphics APIs (Vulkan, Direct3D 12, Metal). The cornerstone of this architecture is an abstraction layer called RenderingDevice, used by all modern renderers. RenderingDevice aims to fit the APIs of different graphics backends into a common interface, allowing compilation to multiple platforms with the same codebase. This layer manages essential GPU resources and operations, such as command lists (draw_list, compute_list, raytracing_list), which enable high-level visual assets (meshes, materials, lights) managed by the RenderingServer to be sent to the GPU efficiently. While this architectural approach aims to reduce code duplication and facilitate the integration of new backends, it also brings a significant CPU load hidden behind every API call. Each render command, shader change, or state change requires a significant amount of CPU work for the GPU to validate its current configuration; this necessitates the use of "batching" techniques to minimize expensive API calls and render similar objects in a single operation, which is the primary goal of render optimizations.
Synchronization between the CPU and GPU is critical for the render process to run smoothly, but it is also a major potential source of CPU bottlenecks. Godot supports both synchronous and asynchronous synchronization mechanisms. The RenderingDevice class offers explicit synchronization points that stall the CPU. For example, the sync() method blocks the calling thread without waiting for the GPU to complete all tasks, which is costly in terms of performance. Similarly, the force_sync() method can also cause performance degradation by creating a mandatory CPU-GPU synchronization point. Asynchronous operations, such as functions like texture_get_data_async used to read a texture from the GPU, prevent the CPU from 525 7 533 1619 533 21
wasting idle time while waiting for the GPU to finish. However, such operations also have a frame delay, and this delay can consume CPU time determined by the rendering/rendering_device/vsync/frame_queue_size project setting. Furthermore, each texture_get_data_async call can take approximately 4 ms of CPU time; initiating a large number of requests simultaneously can increase the total CPU load, leading to a performance drop in the opposite direction. This situation demonstrates the importance of striking a careful balance in asynchronous programming.
One of the most notable findings is that the synchronization logic present in the Vulkan backend has a fundamental flaw. In the cited sources, it has been revealed that the RenderingDeviceVulkan::swap_buffers function does not follow the correct order, meaning it does not wait for the GPU to release resources from the previous frame while preparing a new frame, which can lead to potential "use-after-free" errors and serious crash conditions such as DEVICE_LOST. This flaw stands out not just as a performance issue, but as an architectural deficiency that could jeopardize the stability and reliability of the engine. Consequently, although the RenderingDevice layer provides platform independence, every command passing through it is a CPU load, and the combination of errors in synchronization logic and the use of asynchronous APIs is a delicate balancing act that determines CPU performance.
Synchronization Method / Point | Description | Potential CPU Bottleneck |
|---|---|---|
RenderingDevice.submit() (For Local Devices) | Submits command lists to the GPU from worker threads running outside the primary render thread. | Thread synchronization and command submission cost. |
RenderingDevice.sync() (For Local Devices) | Waits for worker threads running outside the primary render thread to complete. | CPU stalling while waiting for the GPU to finish its work, resulting in CPU time loss. |
RenderingDevice.force_sync() | Triggers a mandatory CPU-GPU synchronization point. | CPU stalling while waiting for the GPU to finish its current work. Highly costly. |
RenderingDevice.texture_get_data_async() | Asynchronously pulls data of a texture on the GPU to the CPU. | Latency, CPU inactivity while waiting for the GPU to have the data ready. |
RenderingDevice.buffer_get_data_async() | Asynchronously pulls data of a buffer on the GPU to the CPU. | Waiting time for the CPU while waiting for data to arrive. |
Vulkan Backend Synchronization Error | swap_buffers may start a new frame without waiting in the correct order. | "use-after-free" risk and DEVICE_LOST due to attempting to release resources the GPU has not finished with yet. |
Another important layer in the architecture is the RenderingServer. This server operates independently of the Node-based scene tree and is used for the creation and management of all visual content. The RenderingServer's API is based on unique identifiers called RID (Resource ID) 32 33 32 4 533 21 32 33 5 4 821
These IDs are opaque pointers rather than actual memory references and enable the creation, update, and deletion of resources. This design allows developers who bypass the scene tree and use the RenderingServer directly to work with a more efficient system, stripped of the costs of the tree system (node lifecycle notifications, transform propagation, etc.). For example, updating the transform of an RID-based object with instance_set_transform() is much faster than doing the same with Node3D.set_transform(). This distinction offers a critical tool for applications aiming to maximize CPU performance.
RenderingServer also allows for safe calling from the main application thread using mechanisms like call_on_render_thread(Callable). This enables the main thread to send tasks to a thread dedicated to render operations, but this submission process is also a synchronization point and consumes CPU time. Therefore, it is important to remember that while RenderingServer offers a more efficient path, the mechanisms required to use it also carry CPU costs. CPU-time measurements can be performed with functions like get_frame_setup_time_cpu(), which provides the total CPU time spent preparing a frame, serving as a valuable indicator for understanding the overall impact of operations within the RenderingServer. In conclusion, as much as Godot's render architecture offers platform independence and flexibility, it is a complex structure that requires precision in CPU-GPU synchronization, contains potential flaws, and necessitates correct optimization strategies.
CPU Bottlenecks and Optimization Trade-offs in the 2D Render Process

The 2D render process in Godot generally exhibits different performance dynamics compared to 3D rendering because it works with simpler geometry sets and fewer materials. 2D games, especially in top-down or side-scroller style projects where thousands of sprites are visible on screen at once, tend to remain bottlenecked on the CPU side. This situation indicates that the most fundamental CPU bottleneck in 2D render processes is sending individual GPU Draw Calls for hundreds or thousands of separate objects. When each draw call is passed through the API interface (OpenGL, Vulkan, etc.), it requires a significant amount of CPU work to validate the GPU's current state (active shader, material, vertex format, etc.). These validation operations are characterized as background overhead executed behind the API, and this cost increases exponentially with state switching performed per object. Therefore, the primary goal of 2D render optimizations is to minimize these expensive 721 22 22 8 8 11 1619 19
API calls and use "batching" techniques to render similar objects in a single operation. Godot offers the Use Batching feature as a solution to this problem. This feature works by combining sprites with similar materials, or other 2D elements like Polygon2D or CollisionPolygon2D, into a single vertex buffer. In this way, many separate sprites can be rendered with a single draw call, which results in a significant reduction in the number of draw calls and, consequently, the API validation load. This process requires a certain amount of CPU cost momentarily; because in each frame, the transforms of the sprites must be retrieved, vertex data must be prepared, and placed into a single buffer. However, this momentary CPU cost is much lower than the gain obtained by making significantly fewer draw calls and generally improves overall CPU performance.
However, the batching technique itself carries the risk of creating new potential bottlenecks on the CPU side. Specifically, a method called "buffer orphaning" is used. In this method, the same vertex buffer can be reused hundreds of times within a single frame. This is an effective way to reduce the cost of memory allocation/deallocation in scenes with dynamic content. However, this technique can yield inconsistent results depending on the platform and graphics driver due to the ambiguities of OpenGL standards regarding dynamic buffer management. Some drivers cannot optimize this constant re-use and can cause GPU driver stalls, which can degrade performance. This emphasizes that an optimization can exhibit platform-specific behavior and that specific tests must be conducted for every project.
For example, a user report showed that on an Intel UHD 620 integrated GPU on the Windows operating system, batching reduced performance from 60 FPS to 45 FPS, while there was no negative impact on Linux on the same hardware. This shows that although 2D batching is a powerful optimization tool, developers must carefully evaluate the impact and potential risks of this feature. The CPU cost of batching usually remains small unless tens of thousands of items are processed, but the bottleneck usually shifts to the graphics driver level during the vertex buffer upload stage.
The most striking 2D CPU bottleneck example that developers can easily miss is the recreation of the mesh in every frame during animation in the Polygon2D class. This situation is called "mesh churn" and can create a massive CPU cost. In a test scenario, for a Polygon2D object animated with AnimationPlayer, freeing the existing mesh and reallocating (creating) it with new vertex data in every frame resulted in a frame time of 35 ms/frame, compared to the optimization that increased FPS from 28 to 83, which had a 15.7 millisecond CPU cost. This is a vital example that optimizing performance sometimes requires focusing on non-intuitive but fundamental data structure manipulation. (116 1 2 1 11 1 1 1 1 1 1130)
When optimization was performed by updating the existing vertex data rather than creating a new mesh, performance increases exceeding 200% were observed. This situation proves how critical the impact of how data structures are managed is on CPU performance. Cache-friendly data structures and data updates without creating waste are a fundamental principle for preventing CPU bottlenecks, especially in 2D rendering.
Batching may not be able to combat fundamental structural errors like mesh churn; because while batching aims to group already prepared vertex data, mesh churn incorrectly handles how this data is created in every frame. Consequently, 2D render optimizations are a trade-off aimed at reducing the number of draw calls. Batching is the most common tool for this trade-off, but it has platform-specific costs and risks. The biggest CPU risk for developers is creating definite bottlenecks like "mesh churn" by managing data structures incorrectly. Such errors should be prevented by focusing on fundamental data manipulation practice rather than non-intuitive and complex-looking optimizations.
Optimization Technique | Goal | CPU Cost (Momentary) | CPU Cost (Long-Term) | Risks and Platform Dependencies |
|---|---|---|---|---|
Reducing Draw Call Count | Lowering API interface validation and state switching cost. | Low (usually negligible) | Very High (Performance gain) | Information not available |
2D Batching | Rendering sprites with similar materials with a single draw call by collecting them into a single vertex buffer. | Medium (Preparation and collection of vertex data) | Very High (Much fewer draw calls) | Performance fluctuations depending on GPU driver, driver stalls due to buffer orphaning. |
Sprite Atlas | Placing multiple sprites onto a single large texture (atlas). | Low (Calculation of atlas coordinates) | High (Reduces draw call count by reducing material change count). | Atlas size limitations, complexity of atlas management. |
Mesh Churn (Bad Practice) | Freeing and recreating the mesh in every frame due to animation or dynamic changes. | Very High (Constant free/allocate of vertex arrays) | Negative (Massive CPU cost, FPS drop) | Can potentially occur if animation is performed with any 2D object in the scene. |
CPU Bottlenecks in the 3D Rendering Process: Culling, Physics, and Transform Management
The 3D rendering process involves a much more complex set of CPU-intensive operations compared to 2D, and bottlenecks stem from this complexity. These bottlenecks can generally be divided into three main categories: filtering invisible objects (culling), physics simulation and synchronization, and the management of objects and lights in the scene. Culling is a process that provides significant performance gains by preventing invisible objects from being sent to the GPU.

However, these culling operations can create a significant CPU load, especially in cases where 3D scenes contain hundreds of thousands of objects. Culling operations are usually performed by the CPU. First, objects outside the Camera's field of view (Frustum) are filtered. This process is usually very fast, but it is costly to apply to hundreds of thousands of objects even in large scenes. A more sophisticated technique, Occlusion Culling, is the most effective but also the most expensive culling technique.
Godot performs this process on the CPU using a software raytracing library called Embree. The engine rasterizes occluder meshes marked with OccluderInstance3D nodes into a low-resolution depth buffer on the CPU side. Then, it checks whether other objects (occludees) are completely hidden by looking at this buffer. If an object's Axis-Aligned Bounding Box (AABB) is completely hidden by an occluder, it is not rendered. This process is a very CPU-intensive operation, especially for complex occluder meshes and moving occluders in the scene.
Baking occluders and updating the BVH (Bounding Volume Hierarchy) structures of moving occluders every frame creates a significant CPU load. For this reason, occlusion culling works best in indoor, multi-room environments and is less useful in open scenes. Furthermore, the benefit of occlusion culling depends on the renderer used. The Forward+ renderer already performs a depth prepass to reduce overdraw. Therefore, while occlusion culling provides an additional advantage, the Mobile renderer (which does not perform a depth prepass) creates a larger performance difference.11 14 1619 111 1 2 1130
The second major CPU bottleneck, physics simulation and its synchronization with the scene tree, is vital, especially for dynamic scenes. The fact that Jolt Physics became the default with Godot 4.6 has been a major improvement in terms of both speed and stability. Jolt offers more advanced collision detection algorithms (GJK/EPA), more stable constraint solvers, and built-in multithreading support.
This has led to 4.7 times higher FPS in a scene containing 5,000 active physics bodies. However, despite the performance gains brought by Jolt, the state sync mechanism itself, which occurs between the PhysicsServer and the scene tree, can create a bottleneck. This process, called flush_queries, triggers a callback function (_sync_body_state) for every active physics object, updating the transform information of nodes (e.g., RigidBody3D) on the GDScript/C++ side.
Due to the individual Callable calls made per object, this can create a CPU load equivalent to the cost of the physics step function, especially in large scenes containing more than 3,000 objects.(23 2428 24 29 2428 24 2428 12 12 15) The solution to this problem lies in adding "bulk query" functions where you can retrieve the transforms, velocities, etc., of all active objects as a single raw data set, instead of an API aimed at individual queries. This will provide a significant CPU improvement by eliminating the callback cost and the clutter of the API interface. Developers can manually control this automatic state sync by disabling it with body_set_state_sync_callback(Callable()).
The third significant CPU bottleneck is sending the world transform of every object to the GPU in dynamic scenes and matching them with the lights required for the illumination of each object. Updating the transform of a dynamic object involves RenderingServer calls such as instance_set_transform(). This can reduce the CPU usage of the 'Preparing to Render 3D scene' step by 50%, especially in a scene containing more than 6,500 dynamic meshes.
This means that it should be more efficient compared to the RID-based system under the Node system. Object-Light Pairing is also done on the CPU. The fact that each object performs an overlap and visibility mask check with every light in the scene is a costly operation, especially in scenes with a large number of lights and dynamic objects. This calculation consumes CPU time even though it is not rendered in the final output. The key to overcoming these bottlenecks is to use more efficient APIs like MultiMeshInstance.
MultiMeshInstance renders thousands of identical meshes with different transforms much more efficiently, and thus representing 6,500 dynamic meshes with a single MMI can achieve performance similar to rendering from a single plane. This coincides with the performance increase provided by going below the SceneTree and using the RenderingServer directly.
In conclusion, 3D CPU bottlenecks consist of broader and more complex systems such as culling, physics, and dynamic object/light management. While occlusion culling is a powerful optimization, it uses the CPU heavily. Physics and transform management contain potential bottlenecks, especially due to API limitations like flush_queries. The key to overcoming these bottlenecks is to use more efficient APIs like MultiMeshInstance and, if possible, to go below the scene tree and use the RenderingServer directly. 15 15 15 27 22 27 27 22 2227
Bottleneck Category | Specific Process | CPU Cost Source | Optimization Methods |
|---|---|---|---|
Culling | Frustum Culling | Comparing objects' AABBs with the frustum. | Simple and fast, effective even in large scenes. |
Culling | Occlusion Culling | Rasterization with Embree on CPU, creation and updating of occluder BVHs. | Most expensive culling technique, worsens for complex occluder meshes. |
Physics and Synchronization | Physics Simulation (Jolt) | Jolt's algorithm complexity and multithread management. | Generally efficient, but can still have significant CPU usage in large scenes. |
Physics and Synchronization | State Sync (flush_queries) | Individual Callable callback cost for every active physics object. | Bulk query functions (suggestion), disabling automatic sync. |
Object and Light Management | Transform Update | Server calls like instance_set_transform() for every dynamic object. | Using MultiMeshInstance, direct use of RID-based RenderingServer. |
Object and Light Management | Object-Light Pairing | Each object performing overlap and mask checks with every light in the scene. | Reducing the number of lights, restricting distant/shadow casting lights, using LOD. |
Data Structures, Script Performance, and Fundamental Optimization Strategies
The CPU performance of the Godot engine is deeply tied not only to the steps within the render pipeline but also to the fundamental principles the developer follows when writing their scene structure and scripts. The foundation of CPU performance, alongside algorithms, is formed by the suitability of the data structures used for the CPU cache. Random memory access patterns lead to situations known as "cache misses," which create idle times filled with the CPU waiting to fetch data from the slow RAM. For this reason, keeping data sequentially and locally in memory significantly increases the cache hit rate and, consequently, performance.

Although Godot's own service APIs (e.g., RenderingServer) handle such optimizations at the functional layer, it is critically important for developers to prefer cache-friendly data structures in their own code (especially in GDExtensions or complex GDScript structures). The most striking example of a 2D CPU bottleneck is the recreation of the mesh in the Polygon2D class during animation on every frame. This is a vital example of why optimizing performance must focus on fundamental data structure manipulation, which is sometimes counter-intuitive. When optimization is performed by updating existing vertex data rather than creating a new mesh, performance increases exceeding 200% have been observed. 23 2428 12 15 2227 27 14 14 14 1130 11
This proves how critical the impact of how data structures are managed is on CPU performance. Similarly, when there are a large number of nodes (thousands to tens of thousands) in a scene, the SceneTree broadcasting _process() and _physics_process() update notifications to every node is also a CPU cost. Managing these nodes outside the scene (Node.remove_child) or by pausing them (set_process(false)) can reduce this cost. The choice of scripting language is also a factor that significantly affects CPU performance. GDScript is prioritized for ease of use rather than speed. This means it is interpreted via a VM (Virtual Machine) and is not compiled directly into machine code.
This means a higher CPU cost compared to C# and especially C++ modules (GDExtensions). Since C# is .NET-based, it offers good performance thanks to JIT compilation, but it may experience pauses during GC (Garbage Collection) intervals. C++, on the other hand, is the fastest option but requires more complex deployment processes. These performance differences require the correct choice of scripting language, especially for frequently called loops or critical algorithms. Using an offline compiler has been suggested to increase the performance of GDScript. This approach eliminates the overhead of the VM by allowing GDScript code to be compiled into a shared library (e.g., .dll, .so) that can talk to the C ABI during export. However, this is a mandatory obstacle for platforms that do not support dynamic code generation, such as iOS, WebAssembly, and console platforms. This reveals a fundamental contradiction between script-based game development and high-performance in-engine coding.
In light of these fundamental factors, the basic optimization strategies that developers should follow to improve CPU performance become clear. The first of these is to avoid the costs of the SceneTree by using the RenderingServer and PhysicsServer directly. The housekeeping (lifecycle notifications, transform propagation, etc.) that the SceneTree performs for each node can create a significant CPU load in large scenes containing thousands of objects. Creating lighter, RID-based entities using the RenderingServer largely eliminates this cost. This approach is ideal, especially for AI characters, particles, or other high-density objects.
The second strategy is to maximize built-in GPU-accelerated optimizations like MultiMeshInstance. When thousands of identical models (grass, trees, projectiles, etc.) need to be rendered, MultiMeshInstance offers the ability to render these objects with a single draw call, and this is one of the most effective ways to get rid of CPU bottlenecks such as transform updates and object-light pairing in dynamic scenes. The third strategy is to make data structures and algorithms cache-friendly. Not constantly recreating vertex data, lining up data linearly in memory, and avoiding unnecessary memory allocation/conversions reduces the CPU's idle time. 14 14 14 14 14 14 17 17 17 1422 22 2327
Finally, profiling is the only reliable way to determine where these optimizations are made. Godot's own built-in profiler, especially the Rendering section within Frame Time, can be used to analyze render-related CPU costs. However, it has been reported that this profiler cannot accurately measure the wait times of some services (physics, audio). For deeper analysis, timing your own code blocks using high-resolution clocks like Time.get_ticks_usec() or using external C++ profilers like Valgrind (Callgrind) can provide more accurate results for identifying CPU hotspots.
Profiling is the most concrete application of the "measure before you optimize" principle and ensures that the developer spends their time most efficiently. These strategies can be thought of as budget management for increasing CPU performance: saving on the costs of the scene tree, purchasing more efficient resources by using the RenderingServer, and making the best use of every clock cycle of the processor by writing cache-friendly code.
Optimization Strategy | Goal | Application Area | Expected CPU Gain |
|---|---|---|---|
Avoiding SceneTree Costs | Eliminating node lifecycle notifications, transform propagation, and other scene tree hosting overhead. | High-density objects (AI, particles, projectiles). | Savings on scene tree costs, especially significant with thousands of nodes. |
Using MultiMeshInstance | Optimizing the rendering of thousands of identical meshes with different transforms. | Grass, trees, projectiles, debris. | Transform update and object-light pairing costs are eliminated, draw call count is drastically reduced. |
Making Data Structures Cache-Friendly | Minimizing CPU idle time by reducing cache misses. | Entire application, especially vertex manipulation, algorithms. | More efficient use of every CPU clock cycle, overall performance increase. |
Optimizing the Scripting Language | Eliminating CPU costs caused by additional layers like VM or GC. | Critical algorithms, frequently called functions. | C++ (GDExtension) > C# > GDScript performance order. |
Focusing with Profiling | Directing optimization efforts by identifying the areas that affect performance the most (hotspots). | Entire application. | Time and effort are spent on the biggest CPU bottlenecks, avoiding unnecessary optimizations. |
Synthesis: Complex Interactions Determining CPU Performance and Future Perspectives
As a result of this in-depth analysis, Godot 4.7's CPU-side render architecture emerges as a dynamic system formed by a complex interaction of various factors. 11 14 10 14 1014 1422 2327 1114 1417 1014. Understanding the specific CPU bottlenecks, which is the main goal of the research, when evaluated with a general synthesis, leads to several key takeaways that will help developers grasp the engine's behavior. First, the fundamental balance between CPU and GPU is at the center of render optimizations.

In 2D rendering, a CPU bottleneck is more common because the time required for the CPU to prepare a frame (draw call preparation, culling, etc.) is usually longer than the GPU's pixel shading time. In this context, techniques like 2D batching are an effective trade-off that converts CPU cost into GPU cost. In 3D rendering, the situation is more complex; both CPU and GPU bottlenecks can follow one another. While the CPU is busy with culling operations that decide whether objects and lights will be rendered, the GPU is busy with the depth and lighting of visible polygons. This dual relationship shows that every optimization creates an effect elsewhere; for example, occlusion culling can reduce the GPU's shading load by straining the CPU, but this can lead to definite CPU bottlenecks like mesh churn in cases like Polygon2D. Second, every optimization is a trade-off, and it is critically important for the developer to find the right balance according to the needs of their scene. While 2D batching converts CPU cost to GPU cost, high LODs (Levels of Detail) strain the GPU instead of the CPU. Transparent objects cannot be batched like opaque objects or materials because they require back-to-front render order, which inherently makes them more expensive.
These trade-offs can yield different results for each project, meaning the best strategy should usually be determined experimentally, taking into account the content of the scene, the capacity of the target platform, and the user's aesthetic demands. While abstraction layers like RenderingDevice and RenderingServer offer developers more control and performance, they actually create another CPU cost. These layers cover the SceneTree, revealing the necessity of transitioning to a more efficient RID-based approach and the complexity that this approach brings (e.g., manually managing world transforms). This shows that the ease offered by abstraction requires a deeper architectural understanding when performance is critical.
Third, CPU performance depends not only on algorithms but also on the fundamental characteristics of the data structures used. The Polygon2D example proves how critical correct data manipulation is. Data should be updated rather than recreated repeatedly, should be lined up sequentially in memory, and should ensure efficient use of the CPU cache. These principles are vital when writing high-performance code based on C++, such as GDExtensions. Furthermore, the VM-based structure of scripting languages like GDScript brings a natural CPU cost compared to C++ modules, and this may make the use of GDExtension mandatory, especially for performance-critical algorithms. These fundamental structural factors must be optimized before or simultaneously with the render pipeline itself.
When future perspectives are examined, advancements in render technologies aim to change the role of the CPU in the render pipeline. Studies aimed at advanced optimizations like GPU-driven rendering aim for the CPU to send all dispatches required to prepare a frame (for objects, lights, passes) to the GPU and then wait for the GPU to complete the process. This architecture could potentially provide a massive performance increase by minimizing the CPU's idle time. However, this approach creates a necessity to maintain some operations, such as the existing CPU-driven transparent render pass, on the CPU, which could create a new bottleneck for the architecture. Such architectural changes will lead to large-scale restructuring and the modification of existing game flows.
In conclusion, in light of the materials provided, while Godot's CPU-side render architecture represents a flexible structure designed to ensure platform independence, it also harbors a significant amount of CPU performance risk, such as synchronization errors, platform-specific behaviors, and complex optimization trade-offs. By understanding this architecture and these bottlenecks, developers can follow the right strategies to create more efficient, smoother, and higher-performance games.211 2327 1124 16 1619 522 22 1130 14 14
*Information: Images were generated by Google Gemini.
