diff --git a/doc/classes/PhysicsServer2D.xml b/doc/classes/PhysicsServer2D.xml index 86bd29ea25..96d7ed6cf5 100644 --- a/doc/classes/PhysicsServer2D.xml +++ b/doc/classes/PhysicsServer2D.xml @@ -1024,6 +1024,26 @@ Constant to set/get the number of solver iterations for all contacts and constraints. The greater the number of iterations, the more accurate the collisions will be. However, a greater number of iterations requires more CPU power, which can decrease performance. The default value of this parameter is [member ProjectSettings.physics/2d/solver/solver_iterations]. + + Constant to set/get the minimum number of constraints a physics step must have before constraint setup and eligible island solving are distributed across the [WorkerThreadPool]. Below this threshold the work is done on the calling thread instead, avoiding thread dispatch and synchronization overhead that would otherwise outweigh the benefit for light workloads. + How constraint setup compares against this threshold depends on [constant SPACE_PARAM_SOLVER_SETUP_THREADING_MODE]: the static mode uses the raw candidate constraint count, while the predicted modes use an estimate of useful work (candidate count scaled by a recent step's ratio of active, post-prune, to candidate constraints), which reduces unnecessary threading in scenes with many broadphase pairs but few real contacts. Island solving always uses the [i]actual[/i] current post-prune constraint count, and additionally requires more than one island, since each island is a single unit of parallel work (so reaching the threshold does not by itself thread solving). + A value of [code]0[/code] threads as aggressively as possible, that is, whenever there is actual parallel work to distribute. The default value of this parameter is [member ProjectSettings.physics/2d/solver/min_constraints_for_threading]. + + + Constant to set/get how the 2D solver decides whether to distribute constraint [i]setup[/i] across the [WorkerThreadPool] (see the [enum SolverSetupThreadingMode] values). Only affects setup scheduling; island solving always uses the actual post-prune work. The default value of this parameter is [member ProjectSettings.physics/2d/solver/setup_threading_mode]. + + + Constant to set/get how many past steps [constant SOLVER_SETUP_THREADING_PREDICTED_BIASED] maxes its work estimate over (the [i]K[/i] in max-over-K). Clamped to [code]1[/code]–[code]8[/code]; [code]1[/code] makes the biased mode behave like [constant SOLVER_SETUP_THREADING_PREDICTED]. Larger values bias further toward threading (they keep threading for more steps after a heavy one). Has no effect in the static or (unbiased) predicted modes. The default value of this parameter is [member ProjectSettings.physics/2d/solver/setup_prediction_window]. + + + Thread constraint setup purely on the raw candidate constraint count (no prediction). Predictable and free of transition artifacts, but threads setup even for scenes with many broadphase pairs that produce few real contacts. + + + Thread constraint setup based on an estimate of useful work: the candidate count scaled by the [i]previous[/i] completed step's active/candidate ratio. Avoids threading light workloads, but assumes temporal coherence — an abrupt workload change costs one suboptimally-scheduled step before the estimate adapts, and workloads that alternate sparse/dense every step defeat it. This is the default. + + + As [constant SOLVER_SETUP_THREADING_PREDICTED], but takes the maximum estimate over the last [i]K[/i] steps (K = [member ProjectSettings.physics/2d/solver/setup_prediction_window], default 2), biasing toward threading. This trades a costly mis-serialized dense step for a cheap redundantly-threaded sparse step across transitions, which removes the alternating-workload regression at the cost of adapting a step or two slower when a workload becomes persistently sparse. + This is the constant for creating world boundary shapes. A world boundary shape is an [i]infinite[/i] line with an origin point, and a normal. Thus, it can be used for front/behind checks. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index c83ae5b228..084c1ae153 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -2505,10 +2505,25 @@ Default solver bias for all physics contacts. Defines how much bodies react to enforce contact separation. See [constant PhysicsServer2D.SPACE_PARAM_CONTACT_DEFAULT_BIAS]. Individual shapes can have a specific bias value (see [member Shape2D.custom_solver_bias]). + + Minimum number of constraints a 2D physics step must have before constraint setup and eligible island solving are distributed across the [WorkerThreadPool]. Below this threshold the work runs on the calling thread instead, avoiding thread dispatch and synchronization overhead that would otherwise outweigh the benefit for light workloads (such as scenes with many fast, mostly-separated bodies). + How constraint setup compares against this threshold depends on [member physics/2d/solver/setup_threading_mode]: [b]Static[/b] uses the raw candidate constraint count; [b]Predicted[/b] scales the candidate count by the previous completed step's ratio of active (post-prune) to candidate constraints (falling back to the raw count when there is no usable previous-step ratio, e.g. the first step); [b]Predicted Biased[/b] takes the maximum of that estimate over the last [member physics/2d/solver/setup_prediction_window] steps. The predicted modes reduce unnecessary threading in scenes with many broadphase pairs but few real contacts. Island solving always uses the [i]actual[/i] current post-prune constraint count, and additionally requires more than one non-empty island, since each island is a single unit of parallel work, so reaching the threshold does not by itself thread solving. + A value of [code]0[/code] threads as aggressively as possible, that is, whenever there is actual parallel work to distribute. Raising it keeps more steps single-threaded; lowering it threads sooner. See [constant PhysicsServer2D.SPACE_PARAM_SOLVER_MIN_CONSTRAINTS_FOR_THREADING]. + [b]Note:[/b] The setup estimate assumes temporal coherence between consecutive physics steps. An abrupt workload change may cause one step to choose a suboptimal scheduling path, and workloads that repeatedly alternate between sparse and dense contacts can defeat the estimate. For persistent workloads, the estimate typically adapts on the following step. + [b]Note:[/b] This value is read when a physics space is created, so changing it at runtime does not affect existing physics spaces. Use [method PhysicsServer2D.space_set_param] to change it for an existing space. + Maximum broadphase pairing margin, in pixels. Each object's collision pairs are kept alive within a margin that scales with its size (up to this maximum), so large moving objects (such as big [Area2D] sensors) re-pair less often, improving performance in scenes with many large overlapping shapes. Small objects keep a proportionally tiny margin so dense scenes do not accumulate excess pairs. Set to [code]0[/code] to disable and use the legacy pair-count-based margin. [b]Note:[/b] This value is read when a physics space is created, so changing it at runtime does not affect existing physics spaces. + + Number of past steps the [constant PhysicsServer2D.SOLVER_SETUP_THREADING_PREDICTED_BIASED] mode maxes its setup-work estimate over (the [i]K[/i] in max-over-K), clamped to 1–8. [code]1[/code] makes biased behave like plain predicted; larger values bias further toward threading. Only used by the biased mode. See [constant PhysicsServer2D.SPACE_PARAM_SOLVER_SETUP_PREDICTION_WINDOW]. + [b]Note:[/b] This value is read when a physics space is created, so changing it at runtime does not affect existing physics spaces. Use [method PhysicsServer2D.space_set_param] to change it for an existing space. + + + How the 2D solver decides whether to distribute constraint setup across the [WorkerThreadPool]: [code]0[/code] static (raw candidate count), [code]1[/code] predicted (default; scale by the previous step's active/candidate ratio), [code]2[/code] predicted biased (max over the last two steps, biased toward threading). See [enum PhysicsServer2D.SolverSetupThreadingMode] for the trade-offs, and [member physics/2d/solver/min_constraints_for_threading] for the threshold itself. Only affects setup; island solving always uses the actual post-prune work. + [b]Note:[/b] This value is read when a physics space is created, so changing it at runtime does not affect existing physics spaces. Use [method PhysicsServer2D.space_set_param] to change it for an existing space. + Number of solver iterations for all contacts and constraints. The greater the number of iterations, the more accurate the collisions will be. However, a greater number of iterations requires more CPU power, which can decrease performance. See [constant PhysicsServer2D.SPACE_PARAM_SOLVER_ITERATIONS]. diff --git a/modules/godot_physics_2d/godot_space_2d.cpp b/modules/godot_physics_2d/godot_space_2d.cpp index a9461111c2..6ccad5b00f 100644 --- a/modules/godot_physics_2d/godot_space_2d.cpp +++ b/modules/godot_physics_2d/godot_space_2d.cpp @@ -1173,6 +1173,15 @@ void GodotSpace2D::set_param(PhysicsServer2D::SpaceParameter p_param, real_t p_v case PhysicsServer2D::SPACE_PARAM_SOLVER_ITERATIONS: solver_iterations = p_value; break; + case PhysicsServer2D::SPACE_PARAM_SOLVER_MIN_CONSTRAINTS_FOR_THREADING: + solver_min_constraints_for_threading = MAX(0, (int)p_value); + break; + case PhysicsServer2D::SPACE_PARAM_SOLVER_SETUP_THREADING_MODE: + solver_setup_threading_mode = CLAMP((int)p_value, 0, PhysicsServer2D::SOLVER_SETUP_THREADING_PREDICTED_BIASED); + break; + case PhysicsServer2D::SPACE_PARAM_SOLVER_SETUP_PREDICTION_WINDOW: + solver_setup_prediction_window = CLAMP((int)p_value, 1, SOLVER_SETUP_HISTORY_MAX); + break; } } @@ -1196,6 +1205,12 @@ real_t GodotSpace2D::get_param(PhysicsServer2D::SpaceParameter p_param) const { return constraint_bias; case PhysicsServer2D::SPACE_PARAM_SOLVER_ITERATIONS: return solver_iterations; + case PhysicsServer2D::SPACE_PARAM_SOLVER_MIN_CONSTRAINTS_FOR_THREADING: + return solver_min_constraints_for_threading; + case PhysicsServer2D::SPACE_PARAM_SOLVER_SETUP_THREADING_MODE: + return solver_setup_threading_mode; + case PhysicsServer2D::SPACE_PARAM_SOLVER_SETUP_PREDICTION_WINDOW: + return solver_setup_prediction_window; } return 0; } @@ -1221,6 +1236,9 @@ GodotSpace2D::GodotSpace2D() { body_angular_velocity_sleep_threshold = GLOBAL_GET("physics/2d/sleep_threshold_angular"); body_time_to_sleep = GLOBAL_GET("physics/2d/time_before_sleep"); solver_iterations = GLOBAL_GET("physics/2d/solver/solver_iterations"); + solver_min_constraints_for_threading = GLOBAL_GET("physics/2d/solver/min_constraints_for_threading"); + solver_setup_threading_mode = GLOBAL_GET("physics/2d/solver/setup_threading_mode"); + solver_setup_prediction_window = CLAMP((int)GLOBAL_GET("physics/2d/solver/setup_prediction_window"), 1, SOLVER_SETUP_HISTORY_MAX); contact_recycle_radius = GLOBAL_GET("physics/2d/solver/contact_recycle_radius"); contact_max_separation = GLOBAL_GET("physics/2d/solver/contact_max_separation"); contact_max_allowed_penetration = GLOBAL_GET("physics/2d/solver/contact_max_allowed_penetration"); diff --git a/modules/godot_physics_2d/godot_space_2d.h b/modules/godot_physics_2d/godot_space_2d.h index 04b5616af9..d338777156 100644 --- a/modules/godot_physics_2d/godot_space_2d.h +++ b/modules/godot_physics_2d/godot_space_2d.h @@ -100,7 +100,21 @@ class GodotSpace2D { GodotArea2D *area = nullptr; + static constexpr int SOLVER_SETUP_HISTORY_MAX = 8; + int solver_iterations = 0; + int solver_min_constraints_for_threading = 256; + int solver_setup_threading_mode = PhysicsServer2D::SOLVER_SETUP_THREADING_PREDICTED; + // Number of past steps the biased mode maxes over (K). Clamped to [1, MAX]. + int solver_setup_prediction_window = 2; + + // Ring of the last few completed steps' (setup, active) constraint counts, + // used to predict how much useful work this step's setup will contain + // (candidate count is a poor proxy when most pairs don't actually collide). + // A setup count of 0 means no history for that slot -> static-gate fallback. + uint32_t solver_setup_hist_setup[SOLVER_SETUP_HISTORY_MAX] = {}; + uint32_t solver_setup_hist_active[SOLVER_SETUP_HISTORY_MAX] = {}; + uint32_t solver_setup_hist_head = 0; // next write slot; (head-1) is most recent real_t contact_recycle_radius = 0.0; real_t contact_max_separation = 0.0; @@ -163,6 +177,21 @@ class GodotSpace2D { const HashSet &get_objects() const; _FORCE_INLINE_ int get_solver_iterations() const { return solver_iterations; } + _FORCE_INLINE_ int get_solver_min_constraints_for_threading() const { return solver_min_constraints_for_threading; } + _FORCE_INLINE_ int get_solver_setup_threading_mode() const { return solver_setup_threading_mode; } + _FORCE_INLINE_ int get_solver_setup_prediction_window() const { return solver_setup_prediction_window; } + // p_back == 0 is the most recent completed step, 1 the one before it, etc. + _FORCE_INLINE_ uint32_t get_solver_prev_setup_at(uint32_t p_back) const { + return solver_setup_hist_setup[(solver_setup_hist_head + SOLVER_SETUP_HISTORY_MAX - 1 - p_back) % SOLVER_SETUP_HISTORY_MAX]; + } + _FORCE_INLINE_ uint32_t get_solver_prev_active_at(uint32_t p_back) const { + return solver_setup_hist_active[(solver_setup_hist_head + SOLVER_SETUP_HISTORY_MAX - 1 - p_back) % SOLVER_SETUP_HISTORY_MAX]; + } + _FORCE_INLINE_ void set_solver_prev_constraint_counts(uint32_t p_setup, uint32_t p_active) { + solver_setup_hist_setup[solver_setup_hist_head] = p_setup; + solver_setup_hist_active[solver_setup_hist_head] = p_active; + solver_setup_hist_head = (solver_setup_hist_head + 1) % SOLVER_SETUP_HISTORY_MAX; + } _FORCE_INLINE_ real_t get_contact_recycle_radius() const { return contact_recycle_radius; } _FORCE_INLINE_ real_t get_contact_max_separation() const { return contact_max_separation; } _FORCE_INLINE_ real_t get_contact_max_allowed_penetration() const { return contact_max_allowed_penetration; } diff --git a/modules/godot_physics_2d/godot_step_2d.cpp b/modules/godot_physics_2d/godot_step_2d.cpp index 3fb5e68daf..21dd99c6b2 100644 --- a/modules/godot_physics_2d/godot_step_2d.cpp +++ b/modules/godot_physics_2d/godot_step_2d.cpp @@ -248,9 +248,56 @@ void GodotStep2D::step(GodotSpace2D *p_space, real_t p_delta) { /* SETUP CONSTRAINTS / PROCESS COLLISIONS */ - uint32_t total_constraint_count = all_constraints.size(); - WorkerThreadPool::GroupID group_task = WorkerThreadPool::get_singleton()->add_template_group_task(this, &GodotStep2D::_setup_constraint, nullptr, total_constraint_count, -1, true, SNAME("Physics2DConstraintSetup")); - WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group_task); + // Below this many constraints, running constraint setup and island solving + // through the WorkerThreadPool costs more than it saves. So, do it on the + // calling thread instead. Tunable per space + // (SPACE_PARAM_SOLVER_MIN_CONSTRAINTS_FOR_THREADING) and via the project + // setting physics/2d/solver/min_constraints_for_threading. + const uint32_t min_constraints_for_threading = p_space->get_solver_min_constraints_for_threading(); + + // Setup distributes one task per constraint, but candidate count can greatly + // overestimate useful work when most pairs do not produce contacts. Depending + // on the space's mode, gate threading on the raw candidate count (STATIC) or + // on an estimate of useful work: candidate count scaled by a recent step's + // active/candidate ratio (cross-multiplied to avoid division/truncation). + // With no usable history, all modes fall back to the static candidate gate. + const uint32_t total_constraint_count = all_constraints.size(); + bool setup_on_thread_pool = false; + if (total_constraint_count > 1) { + const int mode = p_space->get_solver_setup_threading_mode(); + const uint32_t prev_setup = p_space->get_solver_prev_setup_at(0); + if (mode == PhysicsServer2D::SOLVER_SETUP_THREADING_STATIC || prev_setup == 0) { + setup_on_thread_pool = total_constraint_count >= min_constraints_for_threading; + } else { + // PREDICTED uses just the previous step; PREDICTED_BIASED takes the + // max estimate over the last K steps, trading a costly false-serial + // (running a suddenly-dense setup serially) for a cheap false-thread + // across transitions, which neutralizes the alternating regression. + uint32_t window = 1; + if (mode == PhysicsServer2D::SOLVER_SETUP_THREADING_PREDICTED_BIASED) { + window = (uint32_t)p_space->get_solver_setup_prediction_window(); + } + for (uint32_t back = 0; back < window; back++) { + const uint32_t hist_setup = p_space->get_solver_prev_setup_at(back); + if (hist_setup == 0) { + continue; // no history in this slot + } + const uint32_t hist_active = p_space->get_solver_prev_active_at(back); + if ((uint64_t)total_constraint_count * hist_active >= (uint64_t)min_constraints_for_threading * hist_setup) { + setup_on_thread_pool = true; + break; + } + } + } + } + if (setup_on_thread_pool) { + WorkerThreadPool::GroupID group_task = WorkerThreadPool::get_singleton()->add_template_group_task(this, &GodotStep2D::_setup_constraint, nullptr, total_constraint_count, -1, true, SNAME("Physics2DConstraintSetup")); + WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group_task); + } else { + for (uint32_t i = 0; i < total_constraint_count; i++) { + _setup_constraint(i, nullptr); + } + } { //profile profile_endtime = OS::get_singleton()->get_ticks_usec(); @@ -261,16 +308,42 @@ void GodotStep2D::step(GodotSpace2D *p_space, real_t p_delta) { /* PRE-SOLVE CONSTRAINT ISLANDS */ // WARNING: This doesn't run on threads, because it involves thread-unsafe processing. + // Pre-solve also prunes each island to its constraints that actually produced + // contacts, so afterwards we know how much real solving work there is. + uint32_t active_constraint_count = 0; + uint32_t active_island_count = 0; // islands still non-empty after pruning for (uint32_t island_index = 0; island_index < island_count; ++island_index) { _pre_solve_island(constraint_islands[island_index]); + const uint32_t island_size = constraint_islands[island_index].size(); + active_constraint_count += island_size; + if (island_size > 0) { + ++active_island_count; + } } + // Record this step's counts so the next step can predict its setup work. + p_space->set_solver_prev_constraint_counts(total_constraint_count, active_constraint_count); + /* SOLVE CONSTRAINT ISLANDS */ + // Solving distributes one task per island, so it can only parallelize with + // more than one *non-empty* island: a single unit of work would just pay pool + // overhead. Pre-solve can prune islands to empty, so gate on the post-prune + // non-empty count, not the raw island_count. Also gate on the actual + // (post-prune) constraint count, so scenes with many broadphase pairs but few + // real contacts (fast, mostly-separated bodies) don't thread for nothing. + const bool solve_on_thread_pool = active_island_count > 1 && active_constraint_count >= min_constraints_for_threading; + // WARNING: `_solve_island` modifies the constraint islands for optimization purpose, // their content is not reliable after these calls and shouldn't be used anymore. - group_task = WorkerThreadPool::get_singleton()->add_template_group_task(this, &GodotStep2D::_solve_island, nullptr, island_count, -1, true, SNAME("Physics2DConstraintSolveIslands")); - WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group_task); + if (solve_on_thread_pool) { + WorkerThreadPool::GroupID group_task = WorkerThreadPool::get_singleton()->add_template_group_task(this, &GodotStep2D::_solve_island, nullptr, island_count, -1, true, SNAME("Physics2DConstraintSolveIslands")); + WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group_task); + } else { + for (uint32_t island_index = 0; island_index < island_count; ++island_index) { + _solve_island(island_index, nullptr); + } + } { //profile profile_endtime = OS::get_singleton()->get_ticks_usec(); diff --git a/servers/physics_2d/physics_server_2d.cpp b/servers/physics_2d/physics_server_2d.cpp index a6371903c3..af4e5a57f3 100644 --- a/servers/physics_2d/physics_server_2d.cpp +++ b/servers/physics_2d/physics_server_2d.cpp @@ -819,6 +819,13 @@ void PhysicsServer2D::_bind_methods() { BIND_ENUM_CONSTANT(SPACE_PARAM_BODY_TIME_TO_SLEEP); BIND_ENUM_CONSTANT(SPACE_PARAM_CONSTRAINT_DEFAULT_BIAS); BIND_ENUM_CONSTANT(SPACE_PARAM_SOLVER_ITERATIONS); + BIND_ENUM_CONSTANT(SPACE_PARAM_SOLVER_MIN_CONSTRAINTS_FOR_THREADING); + BIND_ENUM_CONSTANT(SPACE_PARAM_SOLVER_SETUP_THREADING_MODE); + BIND_ENUM_CONSTANT(SPACE_PARAM_SOLVER_SETUP_PREDICTION_WINDOW); + + BIND_ENUM_CONSTANT(SOLVER_SETUP_THREADING_STATIC); + BIND_ENUM_CONSTANT(SOLVER_SETUP_THREADING_PREDICTED); + BIND_ENUM_CONSTANT(SOLVER_SETUP_THREADING_PREDICTED_BIASED); BIND_ENUM_CONSTANT(SHAPE_WORLD_BOUNDARY); BIND_ENUM_CONSTANT(SHAPE_SEPARATION_RAY); @@ -920,6 +927,9 @@ PhysicsServer2D::PhysicsServer2D() { GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "physics/2d/sleep_threshold_angular", PROPERTY_HINT_RANGE, "0,90,0.1,radians_as_degrees"), Math::deg_to_rad(8.0)); GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "physics/2d/time_before_sleep", PROPERTY_HINT_RANGE, "0,5,0.01,or_greater,suffix:s"), 0.5); GLOBAL_DEF(PropertyInfo(Variant::INT, "physics/2d/solver/solver_iterations", PROPERTY_HINT_RANGE, "1,32,1,or_greater"), 16); + GLOBAL_DEF(PropertyInfo(Variant::INT, "physics/2d/solver/min_constraints_for_threading", PROPERTY_HINT_RANGE, "0,4096,1,or_greater"), 256); + GLOBAL_DEF(PropertyInfo(Variant::INT, "physics/2d/solver/setup_threading_mode", PROPERTY_HINT_ENUM, "Static,Predicted,Predicted Biased"), PhysicsServer2D::SOLVER_SETUP_THREADING_PREDICTED); + GLOBAL_DEF(PropertyInfo(Variant::INT, "physics/2d/solver/setup_prediction_window", PROPERTY_HINT_RANGE, "1,8,1"), 2); GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "physics/2d/solver/contact_recycle_radius", PROPERTY_HINT_RANGE, "0,10,0.01,or_greater"), 1.0); GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "physics/2d/solver/contact_max_separation", PROPERTY_HINT_RANGE, "0,10,0.01,or_greater"), 1.5); GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "physics/2d/solver/contact_max_allowed_penetration", PROPERTY_HINT_RANGE, "0.01,10,0.01,or_greater"), 0.3); diff --git a/servers/physics_2d/physics_server_2d.h b/servers/physics_2d/physics_server_2d.h index 2794e7fb62..89d22a5efd 100644 --- a/servers/physics_2d/physics_server_2d.h +++ b/servers/physics_2d/physics_server_2d.h @@ -280,6 +280,15 @@ class PhysicsServer2D : public Object { SPACE_PARAM_BODY_TIME_TO_SLEEP, SPACE_PARAM_CONSTRAINT_DEFAULT_BIAS, SPACE_PARAM_SOLVER_ITERATIONS, + SPACE_PARAM_SOLVER_MIN_CONSTRAINTS_FOR_THREADING, + SPACE_PARAM_SOLVER_SETUP_THREADING_MODE, + SPACE_PARAM_SOLVER_SETUP_PREDICTION_WINDOW, + }; + + enum SolverSetupThreadingMode { + SOLVER_SETUP_THREADING_STATIC, // gate on raw candidate count, no prediction + SOLVER_SETUP_THREADING_PREDICTED, // scale candidate count by previous step's active/candidate ratio + SOLVER_SETUP_THREADING_PREDICTED_BIASED, // as PREDICTED, but max over the last two steps (biased toward threading) }; virtual void space_set_param(RID p_space, SpaceParameter p_param, real_t p_value) = 0; @@ -858,6 +867,7 @@ class PhysicsServer2DManager : public Object { VARIANT_ENUM_CAST(PhysicsServer2D::ShapeType); VARIANT_ENUM_CAST(PhysicsServer2D::SpaceParameter); +VARIANT_ENUM_CAST(PhysicsServer2D::SolverSetupThreadingMode); VARIANT_ENUM_CAST(PhysicsServer2D::AreaParameter); VARIANT_ENUM_CAST(PhysicsServer2D::AreaSpaceOverrideMode); VARIANT_ENUM_CAST(PhysicsServer2D::BodyMode);